Der folgende Code kann Ihnen helfen, die "Gesamtidee" zu verstehen, wie sich diese insert()
unterscheidet 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();
}
Die Ausgabe, die ich bekam, war:
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
Beachte das:
Ein unordered_map
speichert Foo
Objekte immer intern (und nicht etwa Foo *
s) als Schlüssel, die alle zerstört werden, wenn das unordered_map
zerstört wird. Hier waren die unordered_map
internen Schlüssel des Foos 13, 11, 5, 10, 7 und 9.
- Technisch gesehen
unordered_map
speichert unser std::pair<const Foo, int>
Objekt tatsächlich Objekte, die wiederum die Foo
Objekte speichern . Um jedoch die "Gesamtidee" zu verstehen, wie emplace()
unterschiedlich sie ist insert()
(siehe hervorgehobenes Kästchen unten), ist es in Ordnung, sich dieses Objekt vorübergehendstd::pair
als vollständig passiv vorzustellen . Sobald Sie diese verstehen „big picture Idee“ , ist es wichtig, dann zu sichern und zu verstehen , wie die Verwendung dieses Vermittler std::pair
Objekt durch unordered_map
einleiten subtil, aber wichtig, technische.
Einfügen von foo0
, foo1
und foo2
erforderte 2 Aufrufe an einen der Foo
Kopier- / Verschiebungskonstruktoren und 2 Aufrufe an Foo
den Destruktor (wie ich jetzt beschreibe):
ein. Einsetzen jedes von foo0
und foo1
eine temporäre Objekt erzeugt ( foo4
und foo6
sind) , deren destructor sofort wurde dann aufgerufen , nachdem das Einsetzen abgeschlossen. Darüber hinaus wurden die Destruktoren der internen Foo
s der unordered_map ( Foo
s 5 und 7) aufgerufen, als die unordered_map zerstört wurde.
b. Zum Einfügen foo2
haben wir stattdessen zunächst explizit ein nicht temporäres Paarobjekt (aufgerufen pair
) erstellt, das Foo
den Kopierkonstruktor von foo2
( aufgerufen foo8
als internes Mitglied von pair
) aufgerufen hat . Wir haben dann insert()
dieses Paar bearbeitet, was dazu führte, unordered_map
dass der Kopierkonstruktor erneut (on foo8
) aufgerufen wurde, um eine eigene interne Kopie ( foo9
) zu erstellen . Wie bei foo
s 0 und 1 war das Endergebnis zwei Destruktoraufrufe für diese Einfügung, mit dem einzigen Unterschied, dass foo8
der Destruktor nur aufgerufen wurde, wenn wir das Ende von erreicht hatten, main()
anstatt unmittelbar nach insert()
Beendigung aufgerufen zu werden .
Das Einfügen foo3
führte zu nur einem Aufruf des Kopier- / Verschiebungskonstruktors ( foo10
intern im unordered_map
) erstellt und nur zu einem Aufruf des Foo
Destruktors. (Ich werde später darauf zurückkommen).
Für foo11
kamen wir direkt die ganze Zahl 11 , emplace(11, d)
so dass unordered_map
der Anruf würde Foo(int)
Konstruktor während der Ausführung innerhalb seiner ist emplace()
Methode. Anders als in (2) und (3) brauchten wir dazu nicht einmal ein vorbestehendes foo
Objekt. Beachten Sie, dass nur 1 Aufruf eines Foo
Konstruktors aufgetreten ist (der erstellt wurde foo11
).
Wir haben dann die ganze Zahl 12 direkt an übergeben insert({12, d})
. Anders als bei emplace(11, d)
(dieser Rückruf führte nur zu einem Aufruf eines Foo
Konstruktors) führte dieser Aufruf zu insert({12, d})
zwei Aufrufen des Foo
Konstruktors (Erstellen foo12
und foo13
).
Dies zeigt, was der Hauptunterschied zwischen insert()
und emplace()
ist:
Während die Verwendung insert()
fast immer die Konstruktion oder Existenz eines Foo
Objekts im main()
Gültigkeitsbereich erfordert (gefolgt von einer Kopie oder Verschiebung), erfolgt bei Verwendung emplace()
jeder Aufruf eines Foo
Konstruktors vollständig intern im unordered_map
(dh innerhalb des Gültigkeitsbereichs der emplace()
Methodendefinition). Die Argumente für den Schlüssel, an den Sie übergeben, emplace()
werden direkt an einen Foo
Konstruktoraufruf innerhalb unordered_map::emplace()
der Definition weitergeleitet (optionale zusätzliche Details: Dieses neu erstellte Objekt wird sofort in eine der unordered_map
Mitgliedsvariablen integriert, sodass kein Destruktor aufgerufen wird, wenn Ausführungsblätter emplace()
und keine Verschiebungs- oder Kopierkonstruktoren werden aufgerufen).
Hinweis: Der Grund für das " fast " in " fast immer " oben wird in I) unten erläutert.
- Fortsetzung: Der Grund , warum calling
umap.emplace(foo3, d)
genannt Foo
‚s nicht konstanten Copykonstruktor ist folgende: Da wir verwenden emplace()
, der Compiler, der weiß foo3
(eine nicht-const Foo
gemeint Objekt) ein Argument zu einem gewissen sein Foo
Konstruktor. In diesem Fall ist der am besten geeignete Foo
Konstruktor der nicht konstante Kopierkonstruktor Foo(Foo& f2)
. Aus diesem Grund wurde umap.emplace(foo3, d)
ein Kopierkonstruktor aufgerufen, obwohl umap.emplace(11, d)
dies nicht der Fall war.
Epilog:
I. Beachten Sie, dass eine Überlastung von insert()
tatsächlich entspricht emplace()
. Wie auf dieser Seite cppreference.com beschrieben , entspricht die Überladung template<class P> std::pair<iterator, bool> insert(P&& value)
( dh die Überladung (2) insert()
auf dieser Seite cppreference.com) emplace(std::forward<P>(value))
.
II. Wohin von hier aus?
ein. Spielen Sie mit dem obigen Quellcode und der Studiendokumentation für insert()
(z. B. hier ) und emplace()
(z. B. hier ), die online verfügbar sind. Wenn Sie eine IDE wie Eclipse oder NetBeans verwenden, können Sie sich von Ihrer IDE leicht mitteilen lassen, welche Überlastung von insert()
oder emplace()
aufgerufen wird (halten Sie in Eclipse den Mauszeiger für eine Sekunde ruhig über dem Funktionsaufruf). Hier ist noch ein Code zum Ausprobieren:
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}}));
Sie werden bald feststellen, dass die Überlastung des std::pair
Konstruktors (siehe Referenz ), die letztendlich verwendet wird unordered_map
, einen wichtigen Einfluss darauf haben kann, wie viele Objekte kopiert, verschoben, erstellt und / oder zerstört werden und wann dies alles auftritt.
b. Sehen Sie, was passiert, wenn Sie eine andere Containerklasse (z. B. std::set
oder std::unordered_multiset
) anstelle von verwenden std::unordered_map
.
c. Verwenden Sie nun ein Goo
Objekt (nur eine umbenannte Kopie von Foo
) anstelle eines int
als Bereichstyp in einem unordered_map
(dh verwenden Sie unordered_map<Foo, Goo>
anstelle von unordered_map<Foo, int>
) und sehen Sie, wie viele und welche Goo
Konstruktoren aufgerufen werden. (Spoiler: Es gibt einen Effekt, aber er ist nicht sehr dramatisch.)