Il seguente codice può aiutarti a capire l '"idea generale" di come insert()
differisce da 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();
}
L'output che ho ottenuto è stato:
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
Notare che:
Un oggetto unordered_map
memorizza sempre internamente Foo
oggetti (e non, diciamo, Foo *
) come chiavi, che vengono tutti distrutti quando unordered_map
vengono distrutti. Qui, le unordered_map
chiavi interne erano 13, 11, 5, 10, 7 e 9 di Foos.
- Quindi tecnicamente, il nostro
unordered_map
effettivamente immagazzina std::pair<const Foo, int>
oggetti, che a loro volta immagazzinano gli Foo
oggetti. Ma per capire la "grande idea" di come emplace()
differisce insert()
(vedi riquadro evidenziato sotto), va bene immaginare temporaneamente questo std::pair
oggetto come completamente passivo. Una volta compresa questa "idea generale", è importante eseguire il backup e comprendere in che modo l'uso di questo std::pair
oggetto intermedio unordered_map
introduce tecnicismi sottili ma importanti.
Inserendo ciascuno di foo0
, foo1
e foo2
richiesto 2 chiamate a uno dei Foo
costruttori di copia / spostamento e 2 chiamate al Foo
distruttore (come descriverò ora):
un. Inserendo ciascuno di foo0
e foo1
creato un oggetto temporaneo ( foo4
e foo6
, rispettivamente) il cui distruttore veniva quindi immediatamente chiamato dopo l'inserimento completato. Inoltre, anche le Foo
s interne di unordered_map (che sono Foo
s 5 e 7) sono state chiamate ai loro distruttori quando la unordered_map è stata distrutta.
b. Per inserire foo2
, invece, abbiamo prima creato esplicitamente un oggetto coppia non temporaneo (chiamato pair
), che ha chiamato Foo
il costruttore della copia su foo2
(creando foo8
come membro interno di pair
). Abbiamo quindi modificato insert()
questa coppia, che ha portato alla unordered_map
chiamata del costruttore di copie (on foo8
) per creare la propria copia interna ( foo9
). Come per foo
s 0 e 1, il risultato finale sono state due chiamate al distruttore per questo inserimento, con l'unica differenza che foo8
il distruttore veniva chiamato solo quando abbiamo raggiunto la fine main()
anziché essere chiamato immediatamente dopo aver insert()
terminato.
L'implementazione ha foo3
comportato solo 1 chiamata del costruttore copia / sposta (creando foo10
internamente nel unordered_map
) e solo 1 chiamata al Foo
distruttore. (Tornerò su questo più tardi).
Per foo11
, abbiamo passato direttamente l'intero 11 a in emplace(11, d)
modo che unordered_map
chiamerebbe il Foo(int)
costruttore mentre l'esecuzione è nel suo emplace()
metodo. A differenza di (2) e (3), non abbiamo nemmeno bisogno di qualche foo
oggetto pre-uscita per farlo. È importante notare che si è verificata solo 1 chiamata a un Foo
costruttore (che ha creato foo11
).
Abbiamo quindi passato direttamente l'intero 12 a insert({12, d})
. Diversamente da emplace(11, d)
(il cui richiamo ha comportato solo 1 chiamata a un Foo
costruttore), questa chiamata ha insert({12, d})
comportato due chiamate al Foo
costruttore (creazione foo12
e foo13
).
Questo mostra quale sia la principale differenza "grande quadro" tra insert()
ed emplace()
è:
Considerando che l'utilizzo insert()
quasi sempre richiede la costruzione o l'esistenza di un Foo
oggetto main()
nell'ambito (seguito da una copia o da uno spostamento), se si utilizza emplace()
quindi qualsiasi chiamata a un Foo
costruttore viene eseguita interamente internamente unordered_map
(ovvero all'interno dell'ambito della emplace()
definizione del metodo). Gli argomenti per la chiave a cui si passa emplace()
vengono inoltrati direttamente a una Foo
chiamata del costruttore all'interno unordered_map::emplace()
della definizione (dettagli aggiuntivi opzionali: dove questo oggetto di nuova costruzione viene immediatamente incorporato in una delle unordered_map
variabili membro in modo che nessun distruttore venga chiamato quando l'esecuzione viene interrotta emplace()
e non vengono chiamati costruttori di spostamento o copia).
Nota: il motivo del " quasi " in " quasi sempre " sopra è spiegato in I) di seguito.
- continua: Il motivo per cui chiamare il costruttore di copie non const
umap.emplace(foo3, d)
chiamato Foo
è il seguente: Dal momento che stiamo usando emplace()
, il compilatore sa che foo3
(un Foo
oggetto non const ) è pensato per essere un argomento per qualche Foo
costruttore. In questo caso, il Foo
costruttore più adatto è il costruttore della copia non const Foo(Foo& f2)
. Questo è il motivo per cui ha umap.emplace(foo3, d)
chiamato un costruttore di copie mentre umap.emplace(11, d)
non lo ha fatto.
Epilogo:
I. Si noti che un sovraccarico di insert()
è effettivamente equivalente a emplace()
. Come descritto in questa pagina di cppreference.com , il sovraccarico template<class P> std::pair<iterator, bool> insert(P&& value)
(che è il sovraccarico (2) di insert()
in questa pagina di cppreference.com) è equivalente a emplace(std::forward<P>(value))
.
II. Dove andare da qui?
un. Gioca con il codice sorgente sopra e studia la documentazione per insert()
(ad es. Qui ) e emplace()
(ad es. Qui ) che si trova online. Se stai usando un IDE come eclipse o NetBeans, puoi facilmente far sì che il tuo IDE ti dica quale sovraccarico di insert()
o emplace()
viene chiamato (in eclipse, mantieni il cursore del mouse costante sulla chiamata di funzione per un secondo). Ecco qualche altro codice da provare:
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}}));
Vedrai presto che quale sovraccarico del std::pair
costruttore (vedi riferimento ) finisce per essere utilizzato unordered_map
può avere un effetto importante su quanti oggetti vengono copiati, spostati, creati e / o distrutti, nonché quando tutto ciò si verifica.
b. Guarda cosa succede quando usi qualche altra classe di container (es. std::set
O std::unordered_multiset
) invece di std::unordered_map
.
c. Ora usa un Goo
oggetto (solo una copia rinominata di Foo
) anziché un int
tipo di intervallo in un unordered_map
(cioè usa unordered_map<Foo, Goo>
invece di unordered_map<Foo, int>
) e vedi quanti e quali Goo
costruttori sono chiamati. (Spoiler: c'è un effetto ma non è molto drammatico.)