Saya mencoba memahami referensi nilai dan memindahkan semantik C ++ 11.
Apa perbedaan antara contoh-contoh ini, dan mana di antara mereka yang tidak memiliki salinan vektor?
Contoh pertama
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return tmp;
}
std::vector<int> &&rval_ref = return_vector();
Contoh kedua
std::vector<int>&& return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
Contoh ketiga
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
c++
c++11
move-semantics
rvalue-reference
c++-faq
Tarantula
sumber
sumber
std::move()
membuat "salinan."std::move(expression)
tidak membuat apa-apa, itu hanya melemparkan ekspresi ke nilai x. Tidak ada objek yang disalin atau dipindahkan dalam proses evaluasistd::move(expression)
.Jawaban:
Contoh pertama
Contoh pertama mengembalikan sementara yang ditangkap oleh
rval_ref
. Sementara itu akan memperpanjang umurnya di luarrval_ref
definisi dan Anda dapat menggunakannya seolah-olah Anda telah menangkapnya berdasarkan nilai. Ini sangat mirip dengan yang berikut:kecuali bahwa dalam penulisan ulang saya, Anda jelas tidak dapat menggunakan
rval_ref
cara non-const.Contoh kedua
Dalam contoh kedua Anda telah membuat kesalahan run time.
rval_ref
sekarang memegang referensi ke yang rusaktmp
di dalam fungsi. Dengan sedikit keberuntungan, kode ini akan langsung macet.Contoh ketiga
Contoh ketiga Anda kira-kira setara dengan yang pertama. The
std::move
ontmp
tidak perlu dan benar-benar dapat menjadi pessimization kinerja karena akan menghambat optimasi nilai kembali.Cara terbaik untuk mengkode apa yang Anda lakukan adalah:
Praktek terbaik
Yaitu seperti yang Anda lakukan di C ++ 03.
tmp
secara implisit diperlakukan sebagai nilai dalam pernyataan pengembalian. Entah itu akan dikembalikan melalui optimasi nilai-kembali (tidak ada salinan, tidak ada gerakan), atau jika kompilator memutuskan tidak dapat melakukan RVO, maka ia akan menggunakan vektor konstruktor bergerak untuk melakukan pengembalian . Hanya jika RVO tidak dilakukan, dan jika tipe yang dikembalikan tidak memiliki konstruktor pemindahan, maka konstruktor salinan akan digunakan untuk pengembalian.sumber
return my_local;
. Beberapa pernyataan pengembalian ok dan tidak akan menghambat RVO.move
tidak membuat sementara. Ini melemparkan nilai ke nilai x, tidak membuat salinan, membuat apa-apa, tidak merusak apa pun. Contoh itu adalah situasi yang sama persis seperti jika Anda kembali dengan referensi-nilai dan menghapusnyamove
dari baris kembali: Either way Anda punya referensi menggantung ke variabel lokal di dalam fungsi dan yang telah dihancurkan.Tak satu pun dari mereka akan menyalin, tetapi yang kedua akan merujuk ke vektor yang hancur. Referensi yang dinamai rvalue hampir tidak pernah ada dalam kode reguler. Anda menulisnya persis seperti bagaimana Anda menulis salinan di C ++ 03.
Kecuali sekarang, vektor dipindahkan. The pengguna kelas tidak berurusan dengan itu nilai p referensi dalam sebagian besar kasus.
sumber
tmp
tidak dipindahkan kerval_ref
, tetapi ditulis langsungrval_ref
menggunakan RVO (yaitu copy elision). Ada perbedaan antarastd::move
dan menyalin elision. Astd::move
mungkin masih melibatkan beberapa data untuk disalin; dalam kasus vektor, vektor baru sebenarnya dibangun di copy constructor dan data dialokasikan, tetapi sebagian besar array data hanya disalin dengan menyalin pointer (pada dasarnya). Salinan salinan menghindari 100% dari semua salinan.rval_ref
variabel dibangun menggunakan move constructorstd::vector
. Tidak ada copy constructor yang terlibat baik dengan / tanpastd::move
.tmp
diperlakukan sebagai nilai p direturn
pernyataan dalam kasus ini.Jawaban sederhananya adalah Anda harus menulis kode untuk referensi nilai seperti halnya Anda menggunakan kode referensi reguler, dan Anda harus memperlakukan mereka dengan 99% mental yang sama. Ini termasuk semua aturan lama tentang pengembalian referensi (yaitu tidak pernah mengembalikan referensi ke variabel lokal).
Kecuali Anda menulis kelas templat templat yang perlu mengambil keuntungan dari std :: forward dan dapat menulis fungsi generik yang mengambil referensi baik nilai atau nilai, ini lebih atau kurang benar.
Salah satu keuntungan besar untuk memindahkan konstruktor dan memindahkan tugas adalah bahwa jika Anda mendefinisikannya, kompilator dapat menggunakannya dalam kasus-kasus adalah RVO (optimasi nilai kembali) dan NRVO (bernama optimasi nilai pengembalian) gagal dipanggil. Ini cukup besar untuk mengembalikan benda mahal seperti wadah & string dengan nilai secara efisien dari metode.
Sekarang hal-hal yang menarik dengan referensi nilai, adalah Anda juga dapat menggunakannya sebagai argumen untuk fungsi normal. Ini memungkinkan Anda untuk menulis kontainer yang memiliki kelebihan muatan untuk referensi const (const foo & lainnya) dan nilai referensi (foo && lainnya). Sekalipun argumen terlalu berat untuk dilewatkan dengan panggilan konstruktor belaka, masih bisa dilakukan:
Wadah STL telah diperbarui untuk memindahkan kelebihan muatan untuk hampir semua hal (kunci hash dan nilai, penyisipan vektor, dll), dan di sinilah Anda paling sering melihatnya.
Anda juga dapat menggunakannya untuk fungsi normal, dan jika Anda hanya memberikan argumen referensi nilai, Anda dapat memaksa pemanggil untuk membuat objek dan membiarkan fungsi melakukan gerakan. Ini lebih merupakan contoh daripada penggunaan yang benar-benar baik, tetapi di perpustakaan rendering saya, saya telah menetapkan string ke semua sumber daya yang dimuat, sehingga lebih mudah untuk melihat apa yang diwakili setiap objek dalam debugger. Tampilannya kira-kira seperti ini:
Ini adalah bentuk 'abstraksi bocor' tetapi memungkinkan saya untuk mengambil keuntungan dari fakta bahwa saya harus membuat string sudah sebagian besar waktu, dan menghindari membuat salinan lain dari itu. Ini bukan kode berkinerja tinggi tetapi merupakan contoh yang baik tentang kemungkinan orang memahami fitur ini. Kode ini sebenarnya mengharuskan variabel menjadi sementara untuk panggilan, atau std :: move dipanggil:
atau
atau
tetapi ini tidak akan dikompilasi!
sumber
Bukan jawaban semata , tetapi sebuah pedoman. Sebagian besar waktu tidak ada artinya dalam mendeklarasikan
T&&
variabel lokal (seperti yang Anda lakukan denganstd::vector<int>&& rval_ref
). Anda masih harusstd::move()
menggunakannya untuk menggunakanfoo(T&&)
metode tipe. Ada juga masalah yang sudah disebutkan bahwa ketika Anda mencoba mengembalikannyarval_ref
dari fungsi Anda akan mendapatkan referensi standar-untuk-hancur-sementara-kegagalan.Sebagian besar waktu saya akan pergi dengan pola berikut:
Anda tidak memegang referensi untuk mengembalikan objek sementara, sehingga Anda menghindari kesalahan programmer (yang tidak berpengalaman) yang ingin menggunakan objek yang dipindahkan.
Jelas ada (walaupun agak jarang) kasus di mana fungsi benar-benar mengembalikan
T&&
yang merupakan referensi ke objek non-temporer yang dapat Anda pindahkan ke objek Anda.Mengenai RVO: mekanisme ini umumnya bekerja dan kompiler dapat dengan baik menghindari penyalinan, tetapi dalam kasus di mana jalur pengembalian tidak jelas (pengecualian,
if
persyaratan menentukan objek bernama Anda akan kembali, dan mungkin beberapa orang lain) rref adalah penyelamat Anda (bahkan jika berpotensi lebih mahal).sumber
Tak satu pun dari mereka akan melakukan penyalinan tambahan. Bahkan jika RVO tidak digunakan, standar baru mengatakan bahwa memindahkan konstruksi lebih disukai untuk menyalin ketika melakukan pengembalian saya percaya.
Saya percaya bahwa contoh kedua Anda menyebabkan perilaku tidak terdefinisi karena Anda mengembalikan referensi ke variabel lokal.
sumber
Seperti yang telah disebutkan dalam komentar untuk jawaban pertama,
return std::move(...);
konstruk dapat membuat perbedaan dalam kasus selain mengembalikan variabel lokal. Berikut adalah contoh runnable yang mendokumentasikan apa yang terjadi ketika Anda mengembalikan objek anggota dengan dan tanpastd::move()
:Agaknya,
return std::move(some_member);
hanya masuk akal jika Anda benar-benar ingin memindahkan anggota kelas tertentu, misalnya dalam kasus di manaclass C
mewakili objek adaptor berumur pendek dengan satu-satunya tujuan membuat instance daristruct A
.Perhatikan bagaimana
struct A
selalu mendapat disalin dariclass B
, bahkan ketikaclass B
objek adalah R-nilai. Ini karena kompiler tidak memiliki cara untuk mengatakan bahwaclass B
instancestruct A
tidak akan digunakan lagi. Dalamclass C
, compiler tidak memiliki informasi ini daristd::move()
, yang mengapastruct A
akan dipindahkan , kecuali contohclass C
adalah konstan.sumber