Saya telah memikirkan pertanyaan ini sedikit selama empat tahun terakhir. Saya sampai pada kesimpulan bahwa sebagian besar penjelasan tentang push_back
vs. emplace_back
melewatkan gambaran lengkap.
Tahun lalu, saya memberikan presentasi di C ++ Now on Type Deduction in C ++ 14 . Saya mulai berbicara tentang push_back
vs. emplace_back
pada 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_back
atau 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_back
akan membangun objek sementara, yang kemudian akan pindah ke v
sedangkan emplace_back
akan 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_back
akan 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 U
dari T
, Anda mengatakan bahwa U
dapat menyimpan semua informasi T
tanpa kehilangan. Aman dalam hampir semua situasi untuk dilewati T
dan tidak ada yang keberatan jika Anda menjadikannya sebagai U
gantinya. Contoh bagus konstruktor implisit adalah konversi dari std::uint32_t
ke std::uint64_t
. Contoh buruk konversi implisit adalah double
untuk 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_back
dapat memanggil konstruktor eksplisit, melewatkan pointer yang tidak memiliki mengkompilasi dengan baik. Namun, ketika v
keluar dari ruang lingkup, destruktor akan berusaha memanggil delete
pointer itu, yang tidak dialokasikan oleh new
karena 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_back
ke 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.
std::unique_ptr<T>
memiliki konstruktor eksplisit dariT *
. Karenaemplace_back
dapat memanggil konstruktor eksplisit, melewatkan pointer yang tidak memiliki mengkompilasi dengan baik. Namun, ketikav
keluar dari ruang lingkup, destruktor akan berusaha memanggildelete
pointer itu, yang tidak dialokasikan olehnew
karena itu hanya objek tumpukan. Ini mengarah pada perilaku yang tidak terdefinisi.explicit
Konstruktor adalah konstruktor yang memiliki kata kunci yangexplicit
diterapkan padanya. Konstruktor "implisit" adalah konstruktor apa pun yang tidak memiliki kata kunci itu. Dalam kasusstd::unique_ptr
konstruktor dariT *
, implementorstd::unique_ptr
menulis konstruktor itu, tetapi masalah di sini adalah bahwa pengguna jenis itu disebutemplace_back
, yang disebut konstruktor eksplisit. Jika sudahpush_back
, alih-alih memanggil konstruktor itu, itu akan bergantung pada konversi implisit, yang hanya dapat memanggil konstruktor implisit.push_back
selalu memungkinkan penggunaan inisialisasi seragam, yang sangat saya sukai. Misalnya:Di sisi lain,
v.emplace_back({ 42, 121 });
tidak akan berfungsi.sumber
{}
sintaks untuk memanggil konstruktor yang sebenarnya, maka Anda bisa menghapus{}
dan gunakanemplace_back
.{}
sintaks untuk memanggil konstruktor yang sebenarnya. Anda bisa memberikanaggregate
konstruktor yang membutuhkan 2 bilangan bulat, dan konstruktor ini akan dipanggil saat menggunakan{}
sintaks. Intinya adalah bahwa jika Anda mencoba memanggil konstruktor,emplace_back
akan lebih disukai, karena memanggil konstruktor di tempat. Dan karena itu tidak memerlukan jenis yang dapat disalin.emplace
agregat 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.Kompatibilitas mundur dengan kompiler pra-C ++ 11.
sumber
push_back
yang lebih disukai.emplace_back
adalah tidak "besar" versipush_back
. Ini versi yang berpotensi berbahaya . Baca jawaban lainnya.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.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_back
menghasilkan perubahan ukuran di mana titikv[0]
menjadi tidak valid.Implementasi kerja dari kode di atas akan digunakan
push_back()
sebagai gantiemplace_back()
sebagai berikut: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.sumber
push_back
berbeda dalam hal ini?