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_mapmemorizza sempre internamente Foooggetti (e non, diciamo, Foo *) come chiavi, che vengono tutti distrutti quando unordered_mapvengono distrutti. Qui, le unordered_mapchiavi interne erano 13, 11, 5, 10, 7 e 9 di Foos.
- Quindi tecnicamente, il nostro
unordered_mapeffettivamente immagazzina std::pair<const Foo, int>oggetti, che a loro volta immagazzinano gli Foooggetti. Ma per capire la "grande idea" di come emplace()differisce insert()(vedi riquadro evidenziato sotto), va bene immaginare temporaneamente questo std::pairoggetto come completamente passivo. Una volta compresa questa "idea generale", è importante eseguire il backup e comprendere in che modo l'uso di questo std::pairoggetto intermedio unordered_mapintroduce tecnicismi sottili ma importanti.
Inserendo ciascuno di foo0, foo1e foo2richiesto 2 chiamate a uno dei Foocostruttori di copia / spostamento e 2 chiamate al Foodistruttore (come descriverò ora):
un. Inserendo ciascuno di foo0e foo1creato un oggetto temporaneo ( foo4e foo6, rispettivamente) il cui distruttore veniva quindi immediatamente chiamato dopo l'inserimento completato. Inoltre, anche le Foos interne di unordered_map (che sono Foos 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 Fooil costruttore della copia su foo2(creando foo8come membro interno di pair). Abbiamo quindi modificato insert()questa coppia, che ha portato alla unordered_mapchiamata del costruttore di copie (on foo8) per creare la propria copia interna ( foo9). Come per foos 0 e 1, il risultato finale sono state due chiamate al distruttore per questo inserimento, con l'unica differenza che foo8il distruttore veniva chiamato solo quando abbiamo raggiunto la fine main()anziché essere chiamato immediatamente dopo aver insert()terminato.
L'implementazione ha foo3comportato solo 1 chiamata del costruttore copia / sposta (creando foo10internamente nel unordered_map) e solo 1 chiamata al Foodistruttore. (Tornerò su questo più tardi).
Per foo11, abbiamo passato direttamente l'intero 11 a in emplace(11, d)modo che unordered_mapchiamerebbe il Foo(int)costruttore mentre l'esecuzione è nel suo emplace()metodo. A differenza di (2) e (3), non abbiamo nemmeno bisogno di qualche foooggetto pre-uscita per farlo. È importante notare che si è verificata solo 1 chiamata a un Foocostruttore (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 Foocostruttore), questa chiamata ha insert({12, d})comportato due chiamate al Foocostruttore (creazione foo12e 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 Foooggetto main()nell'ambito (seguito da una copia o da uno spostamento), se si utilizza emplace()quindi qualsiasi chiamata a un Foocostruttore 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 Foochiamata 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_mapvariabili 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 Foooggetto non const ) è pensato per essere un argomento per qualche Foocostruttore. In questo caso, il Foocostruttore 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::paircostruttore (vedi riferimento ) finisce per essere utilizzato unordered_mappuò 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::setO std::unordered_multiset) invece di std::unordered_map.
c. Ora usa un Goooggetto (solo una copia rinominata di Foo) anziché un inttipo di intervallo in un unordered_map(cioè usa unordered_map<Foo, Goo>invece di unordered_map<Foo, int>) e vedi quanti e quali Goocostruttori sono chiamati. (Spoiler: c'è un effetto ma non è molto drammatico.)