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.