Kode berikut dapat membantu Anda memahami "ide gambaran besar" tentang insert()
perbedaan dari emplace()
:
#include <iostream>
#include <unordered_map>
#include <utility>
//Foo simply outputs what constructor is called with what value.
struct Foo {
static int foo_counter; //Track how many Foo objects have been created.
int val; //This Foo object was the val-th Foo object to be created.
Foo() { val = foo_counter++;
std::cout << "Foo() with val: " << val << '\n';
}
Foo(int value) : val(value) { foo_counter++;
std::cout << "Foo(int) with val: " << val << '\n';
}
Foo(Foo& f2) { val = foo_counter++;
std::cout << "Foo(Foo &) with val: " << val
<< " \tcreated from: \t" << f2.val << '\n';
}
Foo(const Foo& f2) { val = foo_counter++;
std::cout << "Foo(const Foo &) with val: " << val
<< " \tcreated from: \t" << f2.val << '\n';
}
Foo(Foo&& f2) { val = foo_counter++;
std::cout << "Foo(Foo&&) moving: " << f2.val
<< " \tand changing it to:\t" << val << '\n';
}
~Foo() { std::cout << "~Foo() destroying: " << val << '\n'; }
Foo& operator=(const Foo& rhs) {
std::cout << "Foo& operator=(const Foo& rhs) with rhs.val: " << rhs.val
<< " \tcalled with lhs.val = \t" << val
<< " \tChanging lhs.val to: \t" << rhs.val << '\n';
val = rhs.val;
return *this;
}
bool operator==(const Foo &rhs) const { return val == rhs.val; }
bool operator<(const Foo &rhs) const { return val < rhs.val; }
};
int Foo::foo_counter = 0;
//Create a hash function for Foo in order to use Foo with unordered_map
namespace std {
template<> struct hash<Foo> {
std::size_t operator()(const Foo &f) const {
return std::hash<int>{}(f.val);
}
};
}
int main()
{
std::unordered_map<Foo, int> umap;
Foo foo0, foo1, foo2, foo3;
int d;
//Print the statement to be executed and then execute it.
std::cout << "\numap.insert(std::pair<Foo, int>(foo0, d))\n";
umap.insert(std::pair<Foo, int>(foo0, d));
//Side note: equiv. to: umap.insert(std::make_pair(foo0, d));
std::cout << "\numap.insert(std::move(std::pair<Foo, int>(foo1, d)))\n";
umap.insert(std::move(std::pair<Foo, int>(foo1, d)));
//Side note: equiv. to: umap.insert(std::make_pair(foo1, d));
std::cout << "\nstd::pair<Foo, int> pair(foo2, d)\n";
std::pair<Foo, int> pair(foo2, d);
std::cout << "\numap.insert(pair)\n";
umap.insert(pair);
std::cout << "\numap.emplace(foo3, d)\n";
umap.emplace(foo3, d);
std::cout << "\numap.emplace(11, d)\n";
umap.emplace(11, d);
std::cout << "\numap.insert({12, d})\n";
umap.insert({12, d});
std::cout.flush();
}
Output yang saya dapatkan adalah:
Foo() with val: 0
Foo() with val: 1
Foo() with val: 2
Foo() with val: 3
umap.insert(std::pair<Foo, int>(foo0, d))
Foo(Foo &) with val: 4 created from: 0
Foo(Foo&&) moving: 4 and changing it to: 5
~Foo() destroying: 4
umap.insert(std::move(std::pair<Foo, int>(foo1, d)))
Foo(Foo &) with val: 6 created from: 1
Foo(Foo&&) moving: 6 and changing it to: 7
~Foo() destroying: 6
std::pair<Foo, int> pair(foo2, d)
Foo(Foo &) with val: 8 created from: 2
umap.insert(pair)
Foo(const Foo &) with val: 9 created from: 8
umap.emplace(foo3, d)
Foo(Foo &) with val: 10 created from: 3
umap.emplace(11, d)
Foo(int) with val: 11
umap.insert({12, d})
Foo(int) with val: 12
Foo(const Foo &) with val: 13 created from: 12
~Foo() destroying: 12
~Foo() destroying: 8
~Foo() destroying: 3
~Foo() destroying: 2
~Foo() destroying: 1
~Foo() destroying: 0
~Foo() destroying: 13
~Foo() destroying: 11
~Foo() destroying: 5
~Foo() destroying: 10
~Foo() destroying: 7
~Foo() destroying: 9
Perhatikan itu:
Secara unordered_map
internal selalu menyimpan Foo
objek (dan bukan, katakanlah, Foo *
s) sebagai kunci, yang semuanya dihancurkan ketika unordered_map
dihancurkan. Di sini, unordered_map
kunci internal adalah foo 13, 11, 5, 10, 7, dan 9.
- Jadi secara teknis,
unordered_map
sebenarnya kita menyimpan std::pair<const Foo, int>
objek, yang pada gilirannya menyimpan Foo
objek. Tetapi untuk memahami "gagasan gambaran besar" tentang emplace()
perbedaannya insert()
(lihat kotak yang disorot di bawah), boleh-boleh saja untuk sementara waktu membayangkan std::pair
objek ini sebagai sepenuhnya pasif. Setelah Anda memahami "gagasan gambaran besar" ini, penting untuk mencadangkan dan memahami bagaimana penggunaan std::pair
objek perantara ini dengan unordered_map
memperkenalkan teknis yang halus namun penting.
Memasukkan masing-masing foo0
, foo1
dan foo2
diperlukan 2 panggilan ke salah satu Foo
's copy / memindahkan konstruktor dan 2 panggilan ke Foo
' s destructor (seperti sekarang saya jelaskan):
Sebuah. Memasukkan masing-masing foo0
dan foo1
membuat objek sementara ( foo4
dan foo6
, masing-masing) yang destruktornya kemudian segera dipanggil setelah penyisipan selesai. Selain itu, internal Foo
s unordered_map (yang Foo
s 5 dan 7) juga memiliki destruktor mereka dipanggil ketika unordered_map dihancurkan.
b. Untuk menyisipkan foo2
, kami mula-mula secara eksplisit membuat objek pasangan non-sementara (disebut pair
), yang disebut Foo
sebagai konstruktor salin aktif foo2
(dibuat foo8
sebagai anggota internal pair
). Kami kemudian insert()
mengedit pasangan ini, yang mengakibatkan unordered_map
memanggil konstruktor salinan lagi (aktif foo8
) untuk membuat salinan internalnya sendiri ( foo9
). Seperti dengan foo
0 dan 1, hasil akhirnya adalah dua panggilan destruktor untuk penyisipan ini dengan satu-satunya perbedaan adalah bahwa foo8
destruktor dipanggil hanya ketika kita mencapai akhir main()
daripada dipanggil segera setelah insert()
selesai.
Emplacing foo3
menghasilkan hanya 1 salinan / memindahkan panggilan konstruktor (membuat foo10
internal di unordered_map
) dan hanya 1 panggilan ke Foo
destruktor. (Saya akan kembali ke sini nanti).
Karena foo11
, kami langsung meneruskan bilangan bulat 11 ke emplace(11, d)
sehingga unordered_map
akan memanggil Foo(int)
konstruktor saat eksekusi dalam emplace()
metodenya. Tidak seperti pada (2) dan (3), kami bahkan tidak memerlukan objek yang sudah ada sebelumnya foo
untuk melakukan ini. Yang penting, perhatikan bahwa hanya 1 panggilan ke Foo
konstruktor terjadi (yang dibuat foo11
).
Kami kemudian langsung melewati bilangan bulat 12 ke insert({12, d})
. Berbeda dengan dengan emplace(11, d)
(yang mengingat hanya menghasilkan 1 panggilan ke Foo
konstruktor), panggilan ini insert({12, d})
menghasilkan dua panggilan ke Foo
konstruktor (membuat foo12
dan foo13
).
Ini menunjukkan apa perbedaan "gambaran besar" utama antara insert()
dan emplace()
:
Sedangkan menggunakan insert()
hampir selalu membutuhkan konstruksi atau keberadaan beberapa Foo
objek dalam main()
ruang lingkup (diikuti oleh salinan atau pindah), jika menggunakan emplace()
maka setiap panggilan ke Foo
konstruktor dilakukan sepenuhnya secara internal di unordered_map
(yaitu di dalam ruang lingkup emplace()
definisi metode). Argumen untuk kunci yang Anda lewati emplace()
langsung diteruskan ke Foo
panggilan konstruktor dalam unordered_map::emplace()
definisi (perincian tambahan opsional: tempat objek yang baru dibangun ini segera dimasukkan ke dalam salah satu unordered_map
variabel anggota sehingga tidak ada destruktor yang dipanggil saat daun eksekusi emplace()
dan tidak ada memindahkan atau menyalin konstruktor dipanggil).
Catatan: Alasan " hampir " di " hampir selalu " di atas dijelaskan pada I) di bawah ini.
- melanjutkan: Alasan mengapa memanggil yang
umap.emplace(foo3, d)
disebut Foo
non-const copy constructor adalah sebagai berikut: Karena kita menggunakan emplace()
, compiler tahu bahwa foo3
(objek non-const Foo
) dimaksudkan untuk menjadi argumen untuk beberapa Foo
konstruktor. Dalam hal ini, Foo
konstruktor yang paling pas adalah konstruktor salinan non-const Foo(Foo& f2)
. Inilah sebabnya mengapa umap.emplace(foo3, d)
disebut copy constructor sementara umap.emplace(11, d)
tidak.
Epilog:
I. Perhatikan bahwa satu kelebihan insert()
sebenarnya setara dengan emplace()
. Seperti dijelaskan dalam halaman cppreference.com ini , kelebihan template<class P> std::pair<iterator, bool> insert(P&& value)
(yang kelebihan (2) dari insert()
pada halaman cppreference.com ini) setara dengan emplace(std::forward<P>(value))
.
II Ke mana harus pergi dari sini?
Sebuah. Bermain-main dengan kode sumber di atas dan belajar dokumentasi untuk insert()
(misalnya di sini ) dan emplace()
(misalnya di sini ) yang ditemukan online. Jika Anda menggunakan IDE seperti gerhana atau NetBeans maka Anda dapat dengan mudah mendapatkan IDE untuk memberi tahu Anda yang kelebihan beban insert()
atau emplace()
sedang dipanggil (dalam gerhana, pertahankan kursor mouse Anda stabil selama panggilan fungsi sebentar). Berikut beberapa kode untuk dicoba:
std::cout << "\numap.insert({{" << Foo::foo_counter << ", d}})\n";
umap.insert({{Foo::foo_counter, d}});
//but umap.emplace({{Foo::foo_counter, d}}); results in a compile error!
std::cout << "\numap.insert(std::pair<const Foo, int>({" << Foo::foo_counter << ", d}))\n";
umap.insert(std::pair<const Foo, int>({Foo::foo_counter, d}));
//The above uses Foo(int) and then Foo(const Foo &), as expected. but the
// below call uses Foo(int) and the move constructor Foo(Foo&&).
//Do you see why?
std::cout << "\numap.insert(std::pair<Foo, int>({" << Foo::foo_counter << ", d}))\n";
umap.insert(std::pair<Foo, int>({Foo::foo_counter, d}));
//Not only that, but even more interesting is how the call below uses all
// three of Foo(int) and the Foo(Foo&&) move and Foo(const Foo &) copy
// constructors, despite the below call's only difference from the call above
// being the additional { }.
std::cout << "\numap.insert({std::pair<Foo, int>({" << Foo::foo_counter << ", d})})\n";
umap.insert({std::pair<Foo, int>({Foo::foo_counter, d})});
//Pay close attention to the subtle difference in the effects of the next
// two calls.
int cur_foo_counter = Foo::foo_counter;
std::cout << "\numap.insert({{cur_foo_counter, d}, {cur_foo_counter+1, d}}) where "
<< "cur_foo_counter = " << cur_foo_counter << "\n";
umap.insert({{cur_foo_counter, d}, {cur_foo_counter+1, d}});
std::cout << "\numap.insert({{Foo::foo_counter, d}, {Foo::foo_counter+1, d}}) where "
<< "Foo::foo_counter = " << Foo::foo_counter << "\n";
umap.insert({{Foo::foo_counter, d}, {Foo::foo_counter+1, d}});
//umap.insert(std::initializer_list<std::pair<Foo, int>>({{Foo::foo_counter, d}}));
//The call below works fine, but the commented out line above gives a
// compiler error. It's instructive to find out why. The two calls
// differ by a "const".
std::cout << "\numap.insert(std::initializer_list<std::pair<const Foo, int>>({{" << Foo::foo_counter << ", d}}))\n";
umap.insert(std::initializer_list<std::pair<const Foo, int>>({{Foo::foo_counter, d}}));
Anda akan segera melihat bahwa kelebihan std::pair
konstruktor (lihat referensi ) yang akhirnya digunakan oleh unordered_map
dapat memiliki efek penting pada berapa banyak objek yang disalin, dipindahkan, dibuat, dan / atau dihancurkan serta ketika semua ini terjadi.
b. Lihat apa yang terjadi ketika Anda menggunakan beberapa kelas kontainer lain (misalnya std::set
atau std::unordered_multiset
) sebagai gantinya std::unordered_map
.
c. Sekarang gunakan Goo
objek (hanya salinan berganti nama dari Foo
) alih-alih int
sebagai jenis rentang dalam unordered_map
(yaitu menggunakan unordered_map<Foo, Goo>
alih-alih unordered_map<Foo, int>
) dan melihat berapa banyak dan yang Goo
disebut konstruktor. (Spoiler: ada efek tetapi tidak terlalu dramatis.)