Le code ci-dessous montre l'idée générale de comment insert()
diffère de emplace()
en suivant chaque appel de constructeur et en vous donnant des informations à leur sujet au fur et à mesure qu'ils se produisent. Le code est long mais facile à comprendre, voici donc un résumé pour aller à l'essentiel plus rapidement. Le résumé et un rapide coup d'œil sur le code devraient suffire à le comprendre ainsi que son résultat.
Résumé du code : main()
Le code de l'entreprise insert()
et emplace()
s Foo
dans un unordered_map<Foo,int>
avec des appels tels que " umap.emplace(11, d);
" et " umap.insert({12, d})
". Chaque appel est imprimé dans cout
avant qu'il ne soit exécuté. Le site Foo
la classe utilise static int foo_counter
pour garder la trace du nombre total de Foo
les objets qui ont été construits (ou déplacés, copiés, etc.) jusqu'à présent. Chaque Foo
stocke la valeur (unique) de l'objet foo_counter
au moment de sa création dans sa variable locale val
et l'objet unique avec val
== 8
(par exemple) est appelé " foo8
" ou " Foo
8". Chaque appel de constructeur/destructeur imprime des informations sur l'appel (par exemple, l'appel de Foo(11)
produira " Foo(int) with val: 11
"). La comparaison de cette sortie avec le code fera la différence entre insert()
y emplace()
claire.
Code
#include <iostream>
#include <unordered_map>
#include <utility>
using namespace std;
//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++;
cout << "Foo() with val: " << val << '\n';
}
Foo(int value) : val(value) { foo_counter++;
cout << "Foo(int) with val: " << val << '\n';
}
Foo(Foo& f2) { val = foo_counter++;
cout << "Foo(Foo &) with val: " << val
<< " \tcreated from: \t" << f2.val << '\n';
}
Foo(const Foo& f2) { val = foo_counter++;
cout << "Foo(const Foo &) with val: " << val
<< " \tcreated from: \t" << f2.val << '\n';
}
Foo(Foo&& f2) { val = foo_counter++;
cout << "Foo(Foo&&) moving: " << f2.val
<< " \tand changing it to:\t" << val << '\n';
}
~Foo() { cout << "~Foo() destroying: " << val << '\n'; }
Foo& operator=(const Foo& rhs) {
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
template<> struct std::hash<Foo> {
size_t operator()(const Foo &f) const { return hash<int>{}(f.val); }
};
int main() {
unordered_map<Foo, int> umap;
int d; //Some int that will be umap's value. It is not important.
//Print the statement to be executed and then execute it.
cout << "\nFoo foo0, foo1, foo2, foo3;\n";
Foo foo0, foo1, foo2, foo3;
cout << "\numap.insert(pair<Foo, int>(foo0, d))\n";
umap.insert(pair<Foo, int>(foo0, d));
//Side note: equivalent to: umap.insert(make_pair(foo0, d));
cout << "\numap.insert(move(pair<Foo, int>(foo1, d)))\n";
umap.insert(move(pair<Foo, int>(foo1, d)));
//Side note: equiv. to: umap.insert(make_pair(foo1, d));
cout << "\npair<Foo, int> pair(foo2, d)\n";
pair<Foo, int> pair(foo2, d);
cout << "\numap.insert(pair)\n";
umap.insert(pair);
cout << "\numap.emplace(foo3, d)\n";
umap.emplace(foo3, d);
cout << "\numap.emplace(11, d)\n";
umap.emplace(11, d);
cout << "\numap.insert({12, d})\n";
umap.insert({12, d});
cout.flush();
}
Sortie
Foo foo0, foo1, foo2, foo3;
Foo() with val: 0
Foo() with val: 1
Foo() with val: 2
Foo() with val: 3
umap.insert(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(move(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
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
La vue d'ensemble
La principale différence "globale" entre insert()
y emplace()
est :
Considérant que l'utilisation de insert()
presque † toujours nécessite la construction ou la pré-existence d'un certain Foo
objet dans main()
(suivi d'une copie ou d'un déplacement), si l'on utilise la fonction emplace()
alors tout appel à un Foo
est entièrement réalisée en interne dans le unordered_map
(c'est-à-dire à l'intérieur de la portée de l'option emplace()
la définition de la méthode). Le(s) argument(s) de la clé que vous passez à la méthode emplace()
sont directement transmis à un Foo
dans le cadre de l'appel au constructeur unordered_map::emplace()
(détails supplémentaires facultatifs : où ce nouvel objet construit est immédiatement incorporé dans l'un des éléments suivants unordered_map
de façon à ce qu'aucun destructeur ne soit appelé lorsque l'exécution quitte emplace()
et aucun constructeur de déplacement ou de copie n'est appelé).
† La raison de la " presque " dans " presque toujours "ci-dessus est dû au fait qu'une surcharge de insert()
est en fait équivalent à emplace()
. Comme décrit dans cette page cppreference.com la surcharge template<class P> pair<iterator, bool> insert(P&& value)
(qui est la surcharge (2) de insert()
sur cette page) est équivalent à emplace(forward<P>(value))
. Puisque nous nous intéressons aux différences, je vais ignorer cette surcharge et ne pas mentionner à nouveau cette technicité particulière.
Parcourir le code
Je vais maintenant examiner le code et ses résultats en détail.
- Tout d'abord, remarquez qu'un
unordered_map
stocke toujours en interne Foo
(et non, disons, Foo *
) en tant que clés, qui sont toutes détruites lorsque l'objet unordered_map
est détruit. Ici, le unordered_map
Les clés internes de l'entreprise étaient les foos 13, 11, 5, 10, 7 et 9.
- Donc techniquement, notre
unordered_map
stocke effectivement pair<const Foo, int>
qui, à leur tour, stockent les objets Foo
objets. Mais pour comprendre la "grande idée" de comment emplace()
diffère de insert()
(voir l'encadré ci-dessus), il est possible de temporairement imaginez ceci pair
l'objet comme étant entièrement passif. Une fois que vous avez compris cette "idée globale", il est important de revenir en arrière et de comprendre comment l'utilisation de cet intermédiaire pair
objet par unordered_map
introduit des détails techniques subtils, mais importants.
-
insert()
chacun des foo0
, foo1
y foo2
a nécessité 2 appels à l'un des Foo
et 2 appels aux constructeurs copy/move de l'utilisateur. Foo
(comme je le décris maintenant) :
-
insert()
chacun des foo0
y foo1
a créé un objet temporaire ( foo4
y foo6
respectivement) dont le destructeur est appelé immédiatement après la fin de l'insertion. En outre, le unordered_map
interne de l'entreprise Foo
(qui sont foo
s 5 et 7) ont également vu leurs destructeurs appelés lorsque la fonction unordered_map
a été détruit une fois que l'exécution a atteint la fin de main()
.
- A
insert()
foo2
nous avons d'abord créé explicitement un objet paire non temporaire (appelé pair
), qui a appelé Foo
Le constructeur de copie de foo2
(création foo8
en tant que membre interne de pair
). Ensuite, nous insert()
cette paire, ce qui a donné lieu à unordered_map
en appelant à nouveau le constructeur de la copie (sur foo8
) pour créer sa propre copie interne ( foo9
). Comme pour foo
s 0 et 1, le résultat final était deux appels au destructeur pour ce fichier insert()
la seule différence étant que foo8
a été appelé seulement lorsque nous avons atteint la fin de l'article. main()
plutôt que d'être appelé immédiatement après insert()
terminé.
-
emplace()
ing foo3
n'a entraîné qu'un seul appel au constructeur de copie/déplacement (création de foo10
en interne dans le unordered_map
) et seulement 1 appel à Foo
Le destructeur de l'entreprise. La raison pour laquelle l'appel umap.emplace(foo3, d)
appelé Foo
Le constructeur de copie non-const de l'utilisateur est le suivant : Puisque nous utilisons emplace()
le compilateur sait que foo3
(un non-const Foo
) est destiné à être un argument pour un objet Foo
constructeur. Dans ce cas, le plus approprié est Foo
Le constructeur est le constructeur de copie non-const. Foo(Foo& f2)
. C'est pourquoi umap.emplace(foo3, d)
a appelé un constructeur de copie alors que umap.emplace(11, d)
ne l'a pas fait.
-
Pour foo11
nous avons directement passé l'entier 11 à emplace(11, d)
de sorte que unordered_map
appellerait le Foo(int)
pendant que l'exécution est dans son emplace()
méthode. Contrairement à ce qui se passe dans (2) et (3), nous n'avons même pas eu besoin d'un pré-existant. foo
pour ce faire. Il est important de noter que seul un appel à un objet Foo
est apparu (ce qui a créé foo11
).
-
Nous avons ensuite transmis directement l'entier 12 à insert({12, d})
. Contrairement à emplace(11, d)
(dont le rappel n'a donné lieu qu'à un seul appel à une Foo
), cet appel à insert({12, d})
a donné lieu à deux appels à Foo
(qui crée foo12
y foo13
).
Epilogue
Où aller à partir de maintenant ?
a. Jouez avec le code source ci-dessus et étudiez la documentation relative aux éléments suivants insert()
(par exemple aquí ) et emplace()
(par exemple aquí ) que l'on trouve en ligne. Si vous utilisez un environnement de développement intégré (IDE) tel qu'eclipse ou NetBeans, vous pouvez facilement faire en sorte que votre IDE vous indique quelle surcharge de la fonction insert()
o emplace()
est appelé (dans eclipse, il suffit de maintenir le curseur de votre souris sur l'appel de fonction pendant une seconde). Voici un peu plus de code à essayer :
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!
cout << "\numap.insert(pair<const Foo, int>({" << Foo::foo_counter << ", d}))\n";
umap.insert(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?
cout << "\numap.insert(pair<Foo, int>({" << Foo::foo_counter << ", d}))\n";
umap.insert(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 { }.
cout << "\numap.insert({pair<Foo, int>({" << Foo::foo_counter << ", d})})\n";
umap.insert({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;
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}});
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(initializer_list<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".
cout << "\numap.insert(initializer_list<pair<const Foo, int>>({{" << Foo::foo_counter << ", d}}))\n";
umap.insert(initializer_list<pair<const Foo, int>>({{Foo::foo_counter, d}}));
Vous verrez bientôt que toute surcharge de la fonction pair
(voir référence ) finit par être utilisé par unordered_map
peut avoir un effet important sur le nombre d'objets copiés, déplacés, créés et/ou détruits, ainsi que sur le moment où tout cela se produit.
b. S set
o unordered_multiset
) au lieu de unordered_map
.
c. Utilisez maintenant un Goo
(juste une copie renommée de Foo
) au lieu d'un int
comme type de plage dans un unordered_map
(c'est-à-dire utiliser unordered_map<Foo, Goo>
au lieu de unordered_map<Foo, int>
) et voir combien et quelles Goo
les constructeurs sont appelés. (Spoiler : il y a un effet mais il n'est pas très spectaculaire).