Mengembalikan unique_ptr dari fungsi

367

unique_ptr<T>tidak mengizinkan pembuatan salinan, melainkan mendukung pemindahan semantik. Namun, saya dapat mengembalikan unique_ptr<T>dari fungsi dan menetapkan nilai yang dikembalikan ke variabel.

#include <iostream>
#include <memory>

using namespace std;

unique_ptr<int> foo()
{
  unique_ptr<int> p( new int(10) );

  return p;                   // 1
  //return move( p );         // 2
}

int main()
{
  unique_ptr<int> p = foo();

  cout << *p << endl;
  return 0;
}

Kode di atas mengkompilasi dan berfungsi sebagaimana dimaksud. Jadi bagaimana garis 1itu tidak memanggil copy constructor dan menghasilkan kesalahan kompilator? Jika saya harus menggunakan jalur, 2itu lebih masuk akal (menggunakan jalur juga 2berfungsi, tapi kami tidak diharuskan melakukannya).

Saya tahu C ++ 0x memungkinkan pengecualian ini unique_ptrkarena nilai kembali adalah objek sementara yang akan dihancurkan segera setelah fungsi keluar, sehingga menjamin keunikan pointer yang dikembalikan. Saya ingin tahu tentang bagaimana ini diterapkan, apakah itu khusus dimasukkan dalam kompiler atau ada beberapa klausa lain dalam spesifikasi bahasa yang dieksploitasi ini?

Praetorian
sumber
Secara hipotesis, jika Anda menerapkan metode pabrik , apakah Anda lebih suka 1 atau 2 untuk mengembalikan output pabrik? Saya berasumsi bahwa ini akan menjadi penggunaan yang paling umum dari 1 karena, dengan pabrik yang tepat, Anda benar-benar ingin kepemilikan barang yang dibangun untuk diteruskan ke penelepon.
Xharlie
7
@ Xharlie? Keduanya melewati kepemilikan unique_ptr. Seluruh pertanyaan adalah tentang 1 dan 2 menjadi dua cara berbeda untuk mencapai hal yang sama.
Praetorian
dalam hal ini, RVO terjadi di c ++ 0x juga, penghancuran objek unique_ptr akan dilakukan sekali setelah mainfungsi keluar, tetapi tidak ketika fookeluar.
ampawd

Jawaban:

219

apakah ada klausa lain dalam spesifikasi bahasa yang dieksploitasi ini?

Ya, lihat 12.8 §34 dan §35:

Ketika kriteria tertentu terpenuhi, implementasi diperbolehkan untuk menghilangkan copy / memindahkan konstruksi objek kelas [...] Elisi operasi copy / pindah ini, disebut copy elision , diizinkan [...] dalam pernyataan kembali di sebuah fungsi dengan tipe pengembalian kelas, ketika ekspresi adalah nama objek otomatis yang tidak mudah menguap dengan tipe cv-wajar yang sama dengan tipe pengembalian fungsi [...]

Ketika kriteria untuk pemilihan operasi penyalinan terpenuhi dan objek yang akan disalin ditunjuk oleh nilai lebih, resolusi kelebihan untuk memilih konstruktor untuk penyalinan pertama kali dilakukan seolah-olah objek tersebut ditentukan oleh nilai .


Hanya ingin menambahkan satu poin lagi bahwa pengembalian dengan nilai harus menjadi pilihan default di sini karena nilai yang disebutkan dalam pernyataan pengembalian dalam kasus terburuk, yaitu tanpa elisi dalam C ++ 11, C ++ 14 dan C ++ 17 diperlakukan sebagai nilai. Jadi misalnya fungsi berikut mengkompilasi dengan -fno-elide-constructorsflag

std::unique_ptr<int> get_unique() {
  auto ptr = std::unique_ptr<int>{new int{2}}; // <- 1
  return ptr; // <- 2, moved into the to be returned unique_ptr
}

...

auto int_uptr = get_unique(); // <- 3

Dengan flag yang diatur pada kompilasi ada dua gerakan (1 dan 2) yang terjadi di fungsi ini dan kemudian satu gerakan di kemudian hari (3).

fredoverflow
sumber
@ juanchopanza Apakah pada dasarnya Anda berarti bahwa foo()memang juga akan dihancurkan (jika tidak ditugaskan untuk apa pun), seperti nilai kembali dalam fungsi, dan karenanya masuk akal bahwa C ++ menggunakan konstruktor bergerak ketika melakukan unique_ptr<int> p = foo();?
sapi
1
Jawaban ini mengatakan suatu implementasi diperbolehkan untuk melakukan sesuatu ... itu tidak mengatakan itu harus, jadi jika ini adalah satu-satunya bagian yang relevan, yang akan menyiratkan mengandalkan perilaku ini tidak portabel. Tapi saya pikir itu tidak benar. Saya cenderung berpikir bahwa jawaban yang benar lebih berkaitan dengan langkah konstruktor, seperti yang dijelaskan dalam jawaban Nikola Smiljanic dan Bartosz Milewski.
Don Hatch
6
@ DonHatch Dikatakan "diizinkan" untuk melakukan salin / pindahkan elision dalam kasus-kasus itu, tetapi kita tidak berbicara tentang salin salin di sini. Ini adalah paragraf yang dikutip kedua yang berlaku di sini, yang mendukung pigmen-back aturan copy, tetapi bukan copy elision itu sendiri. Tidak ada ketidakpastian dalam paragraf kedua - ini benar-benar portabel.
Joseph Mansfield
@juanchopanza Saya menyadari ini sekarang 2 tahun kemudian, tetapi apakah Anda masih merasa ini salah? Seperti yang saya sebutkan di komentar sebelumnya, ini bukan tentang copy elision. Kebetulan bahwa dalam kasus-kasus di mana copy elision mungkin berlaku (bahkan jika itu tidak dapat diterapkan dengan std::unique_ptr), ada aturan khusus untuk pertama-tama memperlakukan objek sebagai nilai. Saya pikir ini sepenuhnya setuju dengan apa yang dijawab Nikola.
Joseph Mansfield
1
Jadi mengapa saya masih mendapatkan kesalahan "mencoba referensi fungsi yang dihapus" untuk tipe hanya bergerak saya (dihapus copy konstruktor) ketika mengembalikannya persis dengan cara yang sama seperti contoh ini?
DrumM
104

Ini sama sekali tidak spesifik untuk std::unique_ptr, tetapi berlaku untuk kelas apa saja yang dapat dipindahkan. Ini dijamin oleh aturan bahasa karena Anda kembali berdasarkan nilai. Kompiler mencoba menghilangkan salinan, memanggil pemindah konstruktor jika tidak dapat menghapus salinan, memanggil konstruktor salinan jika tidak dapat bergerak, dan gagal mengkompilasi jika tidak dapat menyalin.

Jika Anda memiliki fungsi yang menerima std::unique_ptrsebagai argumen, Anda tidak akan dapat meneruskannya. Anda harus secara eksplisit memanggil move constructor, tetapi dalam hal ini Anda tidak boleh menggunakan variabel p setelah panggilan ke bar().

void bar(std::unique_ptr<int> p)
{
    // ...
}

int main()
{
    unique_ptr<int> p = foo();
    bar(p); // error, can't implicitly invoke move constructor on lvalue
    bar(std::move(p)); // OK but don't use p afterwards
    return 0;
}
Nikola Smiljanić
sumber
3
@ Fred - yah, tidak juga. Meskipun pbukan sementara, hasil dari foo(), apa yang dikembalikan, adalah; jadi itu adalah nilai dan bisa dipindahkan, yang memungkinkan penugasan menjadi mainmungkin. Saya akan mengatakan Anda salah kecuali bahwa Nikola kemudian tampaknya menerapkan aturan ini untuk pdirinya sendiri yang salah.
Edward Strange
Persis apa yang ingin saya katakan, tetapi tidak dapat menemukan kata-kata. Saya telah menghapus bagian dari jawaban itu karena tidak terlalu jelas.
Nikola Smiljanić
Saya punya pertanyaan: dalam pertanyaan awal, apakah ada perbedaan mendasar antara Garis 1dan Garis 2? Dalam pandangan saya itu sama sejak ketika membangun pdi main, hanya peduli tentang jenis jenis kembalinya foo, kan?
Hongxu Chen
1
@HongxuChen Dalam contoh itu sama sekali tidak ada perbedaan, lihat kutipan dari standar dalam jawaban yang diterima.
Nikola Smiljanić
Sebenarnya, Anda dapat menggunakan p sesudahnya, selama Anda menetapkannya. Sampai saat itu, Anda tidak dapat mencoba referensi konten.
Alan
38

unique_ptr tidak memiliki konstruktor salinan tradisional. Alih-alih ia memiliki "pindahkan konstruktor" yang menggunakan referensi nilai:

unique_ptr::unique_ptr(unique_ptr && src);

Referensi nilai (ampers ganda) hanya akan mengikat nilai. Itu sebabnya Anda mendapatkan kesalahan saat Anda mencoba meneruskan lvalue unique_ptr ke suatu fungsi. Di sisi lain, nilai yang dikembalikan dari fungsi diperlakukan sebagai nilai, sehingga konstruktor pemindahan dipanggil secara otomatis.

Omong-omong, ini akan bekerja dengan benar:

bar(unique_ptr<int>(new int(44));

Unique_ptr sementara di sini adalah nilai.

Bartosz Milewski
sumber
8
Saya pikir intinya adalah lebih, mengapa bisa p- "jelas" sebuah lvalue - diperlakukan sebagai nilai p dalam pernyataan kembali return p;dalam definisi foo. Saya tidak berpikir ada masalah dengan fakta bahwa nilai balik fungsi itu sendiri dapat "dipindahkan".
CB Bailey
Apakah membungkus nilai yang dikembalikan dari fungsi di std :: move berarti akan dipindahkan dua kali?
3
@RodrigoSalazar std :: move hanyalah pemain mewah dari referensi nilai (&) ke referensi nilai (&&). Penggunaan luar biasa dari std :: move pada referensi nilai akan menjadi noop
TiMoch
13

Saya pikir itu dijelaskan dengan sempurna dalam item 25 dari Scott Meyers ' Effective Modern C ++ . Berikut ini kutipannya:

Bagian dari Berkat Standar RVO selanjutnya mengatakan bahwa jika kondisi untuk RVO terpenuhi, tetapi kompiler memilih untuk tidak melakukan copy elision, objek yang dikembalikan harus diperlakukan sebagai nilai. Akibatnya, Standar mensyaratkan bahwa ketika RVO diizinkan, baik penyalinan salinan terjadi atau std::movesecara implisit diterapkan pada objek lokal yang dikembalikan.

Di sini, RVO mengacu pada optimalisasi nilai pengembalian , dan jika kondisi untuk RVO terpenuhi berarti mengembalikan objek lokal yang dideklarasikan di dalam fungsi yang Anda harapkan untuk melakukan RVO , yang juga dijelaskan dengan baik dalam item 25 bukunya dengan merujuk pada standar (di sini objek lokal termasuk objek sementara yang dibuat oleh pernyataan kembali). Pengambilan terbesar dari kutipan adalah apakah copy elision terjadi atau std::movesecara implisit diterapkan pada objek lokal yang dikembalikan . Scott menyebutkan dalam item 25 yang std::movediterapkan secara implisit ketika kompiler memilih untuk tidak menghapus salinan dan programmer tidak harus secara eksplisit melakukannya.

Dalam kasus Anda, kode ini jelas merupakan kandidat untuk RVO karena mengembalikan objek lokal pdan jenisnya psama dengan jenis pengembalian, yang menghasilkan salinan salinan. Dan jika kompiler memilih untuk tidak menghapus salinan, untuk alasan apa pun, std::moveakan menendang ke baris 1.

David Lee
sumber
5

Satu hal yang tidak saya lihat di jawaban lain adalahUntuk mengklarifikasi jawaban lain bahwa ada perbedaan antara mengembalikan std :: unique_ptr yang telah dibuat di dalam suatu fungsi, dan yang telah diberikan ke fungsi itu.

Contohnya bisa seperti ini:

class Test
{int i;};
std::unique_ptr<Test> foo1()
{
    std::unique_ptr<Test> res(new Test);
    return res;
}
std::unique_ptr<Test> foo2(std::unique_ptr<Test>&& t)
{
    // return t;  // this will produce an error!
    return std::move(t);
}

//...
auto test1=foo1();
auto test2=foo2(std::unique_ptr<Test>(new Test));
v010dya
sumber
Disebutkan dalam jawaban oleh fredoverflow - dengan jelas disorot " objek otomatis ". Referensi (termasuk referensi nilai) bukan objek otomatis.
Toby Speight
@TobySpeight Ok, maaf. Saya kira kode saya hanya klarifikasi saja.
v010dya