Mengapa saya menggunakan push_back alih-alih emplace_back?

232

C ++ 11 vektor memiliki fungsi baru emplace_back. Tidak seperti push_back, yang bergantung pada optimisasi kompiler untuk menghindari salinan, emplace_backmenggunakan penerusan yang sempurna untuk mengirim argumen langsung ke konstruktor untuk membuat objek di tempat. Sepertinya saya yang emplace_backmelakukan segalanya push_backdapat melakukan, tetapi beberapa waktu itu akan melakukannya dengan lebih baik (tetapi tidak pernah lebih buruk).

Alasan apa yang harus saya gunakan push_back?

David Stone
sumber

Jawaban:

162

Saya telah memikirkan pertanyaan ini sedikit selama empat tahun terakhir. Saya sampai pada kesimpulan bahwa sebagian besar penjelasan tentang push_backvs. emplace_backmelewatkan gambaran lengkap.

Tahun lalu, saya memberikan presentasi di C ++ Now on Type Deduction in C ++ 14 . Saya mulai berbicara tentang push_backvs. emplace_backpada 13:49, tetapi ada informasi berguna yang menyediakan beberapa bukti pendukung sebelum itu.

Perbedaan utama sebenarnya berkaitan dengan konstruktor implisit vs eksplisit. Pertimbangkan kasus di mana kita memiliki satu argumen yang ingin kita sampaikan push_backatau emplace_back.

std::vector<T> v;
v.push_back(x);
v.emplace_back(x);

Setelah kompiler pengoptimalisasi Anda berhasil, tidak ada perbedaan antara kedua pernyataan ini dalam hal kode yang dihasilkan. Kearifan tradisional adalah bahwa push_backakan membangun objek sementara, yang kemudian akan pindah ke vsedangkan emplace_backakan meneruskan argumen bersama dan membangunnya langsung di tempat tanpa salinan atau bergerak. Ini mungkin benar berdasarkan kode seperti yang ditulis di perpustakaan standar, tetapi itu membuat asumsi yang salah bahwa tugas kompiler mengoptimalkan adalah untuk menghasilkan kode yang Anda tulis. Pekerjaan kompiler pengoptimal sebenarnya untuk menghasilkan kode yang Anda tulis jika Anda seorang ahli optimasi platform spesifik dan tidak peduli tentang rawatan, hanya kinerja.

Perbedaan aktual antara kedua pernyataan ini adalah bahwa semakin kuat emplace_backakan memanggil semua jenis konstruktor di luar sana, sedangkan yang lebih berhati push_back- hati akan memanggil hanya konstruktor yang implisit. Konstruktor implisit seharusnya aman. Jika Anda dapat secara implisit membangun Udari T, Anda mengatakan bahwa Udapat menyimpan semua informasi Ttanpa kehilangan. Aman dalam hampir semua situasi untuk dilewati Tdan tidak ada yang keberatan jika Anda menjadikannya sebagai Ugantinya. Contoh bagus konstruktor implisit adalah konversi dari std::uint32_tke std::uint64_t. Contoh buruk konversi implisit adalah doubleuntuk std::uint8_t.

Kami ingin berhati-hati dalam pemrograman kami. Kami tidak ingin menggunakan fitur yang kuat karena semakin kuat fitur, semakin mudah untuk melakukan sesuatu yang tidak benar atau tidak terduga. Jika Anda bermaksud memanggil konstruktor eksplisit, maka Anda memerlukan kekuatan emplace_back. Jika Anda ingin memanggil konstruktor implisit saja, tetap dengan keamanan push_back.

Sebuah contoh

std::vector<std::unique_ptr<T>> v;
T a;
v.emplace_back(std::addressof(a)); // compiles
v.push_back(std::addressof(a)); // fails to compile

std::unique_ptr<T>memiliki konstruktor eksplisit dari T *. Karena emplace_backdapat memanggil konstruktor eksplisit, melewatkan pointer yang tidak memiliki mengkompilasi dengan baik. Namun, ketika vkeluar dari ruang lingkup, destruktor akan berusaha memanggil deletepointer itu, yang tidak dialokasikan oleh newkarena itu hanya objek tumpukan. Ini mengarah pada perilaku yang tidak terdefinisi.

Ini bukan hanya kode yang ditemukan. Ini adalah bug produksi nyata yang saya temui. Kode itu std::vector<T *>, tetapi memiliki isinya. Sebagai bagian dari migrasi ke C ++ 11, saya benar berubah T *untuk std::unique_ptr<T>menunjukkan bahwa vektor dimiliki memori. Namun, saya mendasarkan perubahan ini dari pemahaman saya pada tahun 2012, di mana saya berpikir "emplace_back melakukan semua yang bisa dilakukan push_back dan banyak lagi, jadi mengapa saya menggunakan push_back?", Jadi saya juga mengubah push_backke emplace_back.

Seandainya saya meninggalkan kode sebagai menggunakan yang lebih aman push_back, saya akan langsung menangkap bug lama ini dan itu akan dilihat sebagai keberhasilan meningkatkan ke C ++ 11. Sebagai gantinya, saya menutupi bug dan tidak menemukannya sampai berbulan-bulan kemudian.

David Stone
sumber
Ini akan membantu jika Anda bisa menguraikan apa yang tepatnya dilakukan emplace dalam contoh Anda, dan mengapa itu salah.
eddi
4
@eddi: Saya menambahkan bagian yang menjelaskan ini: std::unique_ptr<T>memiliki konstruktor eksplisit dari T *. Karena emplace_backdapat memanggil konstruktor eksplisit, melewatkan pointer yang tidak memiliki mengkompilasi dengan baik. Namun, ketika vkeluar dari ruang lingkup, destruktor akan berusaha memanggil deletepointer itu, yang tidak dialokasikan oleh newkarena itu hanya objek tumpukan. Ini mengarah pada perilaku yang tidak terdefinisi.
David Stone
Terima kasih telah memposting ini. Saya tidak tahu tentang hal itu ketika saya menulis jawaban saya, tetapi sekarang saya berharap saya akan menulisnya sendiri ketika saya mempelajarinya sesudahnya :) Saya benar-benar ingin menampar orang yang beralih ke fitur baru hanya untuk melakukan hal paling keren yang dapat mereka temukan . Guys, orang-orang juga menggunakan C ++ sebelum C ++ 11, dan tidak semuanya bermasalah. Jika Anda tidak tahu mengapa Anda menggunakan fitur, jangan gunakan itu . Senang sekali Anda memposting ini dan saya harap mendapat lebih banyak upvotes sehingga melampaui saya. +1
user541686
1
@ KaptenJacksparrow: Sepertinya saya katakan implisit dan eksplisit di mana saya maksudkan mereka. Bagian mana yang membuat Anda bingung?
David Stone
4
@CaptainJacksparrow: explicitKonstruktor adalah konstruktor yang memiliki kata kunci yang explicitditerapkan padanya. Konstruktor "implisit" adalah konstruktor apa pun yang tidak memiliki kata kunci itu. Dalam kasus std::unique_ptrkonstruktor dari T *, implementor std::unique_ptrmenulis konstruktor itu, tetapi masalah di sini adalah bahwa pengguna jenis itu disebut emplace_back, yang disebut konstruktor eksplisit. Jika sudah push_back, alih-alih memanggil konstruktor itu, itu akan bergantung pada konversi implisit, yang hanya dapat memanggil konstruktor implisit.
David Stone
117

push_backselalu memungkinkan penggunaan inisialisasi seragam, yang sangat saya sukai. Misalnya:

struct aggregate {
    int foo;
    int bar;
};

std::vector<aggregate> v;
v.push_back({ 42, 121 });

Di sisi lain, v.emplace_back({ 42, 121 });tidak akan berfungsi.

Luc Danton
sumber
58
Perhatikan bahwa ini hanya berlaku untuk inisialisasi agregat dan inisialisasi daftar-inisialisasi. Jika Anda akan menggunakan {}sintaks untuk memanggil konstruktor yang sebenarnya, maka Anda bisa menghapus {}dan gunakan emplace_back.
Nicol Bolas
Waktu pertanyaan bodoh: jadi emplace_back tidak dapat digunakan untuk vektor struct sama sekali? Atau tidak hanya untuk gaya ini menggunakan literal {42.121}?
Phil H
1
@LucDanton: Seperti yang saya katakan, itu hanya berlaku untuk inisialisasi agregat dan daftar-daftar . Anda dapat menggunakan {}sintaks untuk memanggil konstruktor yang sebenarnya. Anda bisa memberikan aggregatekonstruktor yang membutuhkan 2 bilangan bulat, dan konstruktor ini akan dipanggil saat menggunakan {}sintaks. Intinya adalah bahwa jika Anda mencoba memanggil konstruktor, emplace_backakan lebih disukai, karena memanggil konstruktor di tempat. Dan karena itu tidak memerlukan jenis yang dapat disalin.
Nicol Bolas
11
Ini dipandang sebagai cacat dalam standar, dan telah diselesaikan. Lihat cplusplus.github.io/LWG/lwg-active.html#2089
David Stone
3
@ Davidvidone Seandainya itu diselesaikan, itu tidak akan tetap dalam daftar "aktif" ... tidak? Tampaknya tetap menjadi masalah yang luar biasa. Pembaruan terbaru, menuju " [2018-08-23 Pemrosesan Masalah Batavia] ", mengatakan bahwa " P0960 (saat ini dalam penerbangan) harus menyelesaikan ini. " Dan saya masih tidak dapat mengkompilasi kode yang mencoba untuk emplaceagregat tanpa secara eksplisit menulis konstruktor boilerplate . Pada saat ini juga tidak jelas apakah akan diperlakukan sebagai cacat dan karenanya memenuhi syarat untuk backporting, atau apakah pengguna C ++ <20 akan tetap SoL.
underscore_d
80

Kompatibilitas mundur dengan kompiler pra-C ++ 11.

pengguna541686
sumber
21
Itu sepertinya kutukan C ++. Kami mendapatkan banyak fitur keren dengan setiap rilis baru, tetapi banyak perusahaan yang terjebak menggunakan beberapa versi lama demi kompatibilitas atau mengecilkan (jika tidak diingkari) penggunaan fitur tertentu.
Dan Albert
6
@Mehrdad: Mengapa puas dengan cukup ketika Anda bisa memiliki yang hebat? Saya yakin tidak ingin pemrograman di blub , bahkan jika itu sudah cukup. Tidak mengatakan bahwa itu adalah kasus untuk contoh ini khususnya, tetapi sebagai seseorang yang menghabiskan sebagian besar waktunya untuk pemrograman di C89 demi kompatibilitas, itu jelas merupakan masalah nyata.
Dan Albert
3
Saya rasa ini bukan jawaban untuk pertanyaan itu. Bagi saya dia meminta kasus penggunaan push_backyang lebih disukai.
Tn. Boy
3
@ Mr.Boy: Lebih disukai bila Anda ingin kompatibel dengan terbelakang dengan kompiler pra-C ++ 11. Apakah itu tidak jelas dalam jawaban saya?
user541686
6
Ini telah mendapat cara perhatian lebih dari yang saya harapkan, jadi untuk kalian semua membaca ini: emplace_backadalah tidak "besar" versi push_back. Ini versi yang berpotensi berbahaya . Baca jawaban lainnya.
user541686
68

Beberapa implementasi pustaka emplace_back tidak berlaku seperti yang ditentukan dalam standar C ++ termasuk versi yang dikirimkan dengan Visual Studio 2012, 2013 dan 2015.

Untuk mengakomodasi bug kompiler yang diketahui, lebih baik menggunakan std::vector::push_back()jika parameter merujuk iterator atau objek lain yang akan tidak valid setelah panggilan.

std::vector<int> v;
v.emplace_back(123);
v.emplace_back(v[0]); // Produces incorrect results in some compilers

Pada satu kompiler, v berisi nilai-nilai 123 dan 21 bukannya 123 dan 123 yang diharapkan. Hal ini disebabkan oleh fakta bahwa panggilan ke-2 emplace_backmenghasilkan perubahan ukuran di mana titik v[0]menjadi tidak valid.

Implementasi kerja dari kode di atas akan digunakan push_back()sebagai ganti emplace_back()sebagai berikut:

std::vector<int> v;
v.emplace_back(123);
v.push_back(v[0]);

Catatan: Penggunaan vektor int adalah untuk tujuan demonstrasi. Saya menemukan masalah ini dengan kelas yang jauh lebih kompleks yang mencakup variabel anggota yang dialokasikan secara dinamis dan panggilan untuk emplace_back()menghasilkan crash keras.

Marc
sumber
Tunggu. Tampaknya ini adalah bug. Bagaimana bisa push_backberbeda dalam hal ini?
balki
12
Panggilan ke emplace_back () menggunakan penerusan yang sempurna untuk melakukan konstruksi di tempat dan dengan demikian v [0] tidak dievaluasi sampai setelah vektor diubah ukurannya (pada titik mana v [0] tidak valid). push_back membangun elemen baru dan menyalin / memindahkan elemen sesuai kebutuhan dan v [0] dievaluasi sebelum realokasi apa pun.
Marc
1
@ Markc: Dijamin oleh standar bahwa emplace_back berfungsi bahkan untuk elemen di dalam rentang.
David Stone
1
@ Davidvidone: Bisakah tolong berikan referensi ke mana dalam standar perilaku ini dijamin? Either way, Visual Studio 2012 dan 2015 menunjukkan perilaku yang salah.
Marc
4
@ameino: emplace_back ada untuk menunda evaluasi parameternya untuk mengurangi penyalinan yang tidak perlu. Perilaku tersebut tidak terdefinisi atau bug penyusun (menunggu analisis standar). Saya baru-baru ini menjalankan tes yang sama terhadap Visual Studio 2015 dan mendapatkan 123,3 di bawah Rilis x64, 123,40 di bawah Rilis Win32 dan 123, -572662307 di bawah Debug x64 dan Debug Win32.
Marc