J'utilise des cartes pour la première fois et je me suis rendu compte qu'il existe de nombreuses façons d'insérer un élément. Vous pouvez utiliser emplace()
, operator[]
ou insert()
, plus des variantes comme l'utilisation de value_type
ou make_pair
. Bien qu'il y ait beaucoup d'informations sur chacun d'eux et des questions sur des cas particuliers, je ne comprends toujours pas la situation dans son ensemble. Donc, mes deux questions sont:
Quel est l'avantage de chacun d'eux sur les autres?
Était-il nécessaire d'ajouter un emplacement à la norme? Y a-t-il quelque chose qui n'était pas possible auparavant sans cela?
operator[]
est basé surtry_emplace
. Cela vaut peut-être la peine de le mentionnerinsert_or_assign
.Réponses:
Dans le cas particulier d'une carte, les anciennes options n'étaient que deux:
operator[]
etinsert
(différentes saveurs deinsert
). Je vais donc commencer à les expliquer.Le
operator[]
est un opérateur de recherche ou d'ajout . Il essaiera de trouver un élément avec la clé donnée à l'intérieur de la carte, et s'il existe, il renverra une référence à la valeur stockée. Si ce n'est pas le cas, il créera un nouvel élément inséré en place avec l'initialisation par défaut et retournera une référence à celui-ci.La
insert
fonction (dans la saveur d'élément unique) prend unvalue_type
(std::pair<const Key,Value>
), elle utilise la clé (first
membre) et essaie de l'insérer. Parcestd::map
que ne permet pas les doublons s'il y a un élément existant, il n'insérera rien.La première différence entre les deux est que
operator[]
doit être en mesure de construire une initialisation par défaut la valeur , et il est donc inutilisable pour les types de valeurs qui ne peuvent être réinitialisés par défaut. La deuxième différence entre les deux est ce qui se passe lorsqu'il y a déjà un élément avec la clé donnée. Lainsert
fonction ne modifiera pas l'état de la carte, mais retournera à la place un itérateur à l'élément (et unfalse
indiquant qu'il n'a pas été inséré).// assume m is std::map<int,int> already has an element with key 5 and value 0 m[5] = 10; // postcondition: m[5] == 10 m.insert(std::make_pair(5,15)); // m[5] is still 10
Dans le cas de
insert
l'argument est un objet devalue_type
, qui peut être créé de différentes manières. Vous pouvez le construire directement avec le type approprié ou passer n'importe quel objet à partir duquel levalue_type
peut être construit, ce quistd::make_pair
entre en jeu, car il permet de créer simplement desstd::pair
objets, même si ce n'est probablement pas ce que vous voulez ...L'effet net des appels suivants est similaire :
K t; V u; std::map<K,V> m; // std::map<K,V>::value_type is std::pair<const K,V> m.insert( std::pair<const K,V>(t,u) ); // 1 m.insert( std::map<K,V>::value_type(t,u) ); // 2 m.insert( std::make_pair(t,u) ); // 3
Mais ce ne sont pas vraiment les mêmes ... [1] et [2] sont en fait équivalents. Dans les deux cas, le code crée un objet temporaire du même type (
std::pair<const K,V>
) et le transmet à lainsert
fonction. Lainsert
fonction créera le nœud approprié dans l'arborescence de recherche binaire, puis copiera lavalue_type
partie de l'argument vers le nœud. L'avantage de l'utilisationvalue_type
est que, eh bien, correspondvalue_type
toujours , vous ne pouvez pas mal saisir le type devalue_type
std::pair
arguments!La différence est dans [3]. La fonction
std::make_pair
est une fonction modèle qui créera un fichierstd::pair
. La signature est:template <typename T, typename U> std::pair<T,U> make_pair(T const & t, U const & u );
Je n'ai intentionnellement pas fourni les arguments du modèle
std::make_pair
, car c'est l'usage courant. Et l'implication est que les arguments du modèle sont déduits de l'appel, dans ce cas êtreT==K,U==V
, donc l'appel àstd::make_pair
retournera unstd::pair<K,V>
(notez le manquantconst
). La signature exigevalue_type
que ce soit proche mais différent de la valeur renvoyée par l'appel àstd::make_pair
. Comme il est suffisamment proche, il créera un temporaire du type correct et la copie l'initialisera. Cela sera à son tour copié sur le nœud, créant un total de deux copies.Cela peut être résolu en fournissant les arguments du modèle:
m.insert( std::make_pair<const K,V>(t,u) ); // 4
Mais cela est toujours sujet aux erreurs de la même manière que la saisie explicite du type dans le cas [1].
Jusqu'à présent, nous avons différentes façons d'appeler
insert
qui nécessitent la création de l'value_type
externe et la copie de cet objet dans le conteneur. Vous pouvez également utiliseroperator[]
si le type est constructible et assignable par défaut (se concentrant intentionnellement uniquement dansm[k]=v
), et il nécessite l'initialisation par défaut d'un objet et la copie de la valeur dans cet objet.En C ++ 11, avec des modèles variadiques et une transmission parfaite, il existe une nouvelle façon d'ajouter des éléments dans un conteneur au moyen de la mise en place (création sur place). Les
emplace
fonctions des différents conteneurs font fondamentalement la même chose: au lieu d'obtenir une source à partir de laquelle copier dans le conteneur, la fonction prend les paramètres qui seront transmis au constructeur de l'objet stocké dans le conteneur.m.emplace(t,u); // 5
Dans [5], le
std::pair<const K, V>
n'est pas créé et passé àemplace
, mais plutôt des références à l' objett
etu
sont passées à ceemplace
qui les transmet au constructeur duvalue_type
sous - objet à l'intérieur de la structure de données. Dans ce cas, aucune copie destd::pair<const K,V>
n'est effectuée, ce qui est l'avantage paremplace
rapport aux alternatives C ++ 03. Comme dans le cas,insert
il ne remplacera pas la valeur de la carte.Une question intéressante à laquelle je n'avais pas pensé est de savoir comment
emplace
mettre en œuvre une carte, et ce n'est pas un problème simple dans le cas général.la source
mapped_type
copie séparée. Ce que nous voulons, c'est mettre en place la construction dumapped_type
dans la paire, et mettre en place la construction de la paire dans la carte. Par conséquent, lastd::pair::emplace
fonction et sa prise en charge de transfert dansmap::emplace
sont tous deux manquants. Dans sa forme actuelle, vous devez toujours donner un mapped_type construit au constructeur de paire qui le copiera une fois. c'est mieux que deux fois, mais toujours pas bon.insert_or_assign
ettry_emplace
(tous deux à partir de C ++ 17), qui aident à combler certaines lacunes dans les fonctionnalités des méthodes existantes.Emplace: tire parti de la référence rvalue pour utiliser les objets réels que vous avez déjà créés. Cela signifie qu'aucun constructeur de copie ou de déplacement n'est appelé, bon pour les objets LARGE! O (log (N)) heure.
Insérer: Contient des surcharges pour la référence lvalue standard et la référence rvalue, ainsi que des itérateurs vers les listes d'éléments à insérer et des «indices» quant à la position d'un élément. L'utilisation d'un itérateur "hint" peut ramener le temps d'insertion au temps contant, sinon c'est le temps O (log (N)).
Opérateur []: vérifie si l'objet existe, et si c'est le cas, modifie la référence à cet objet, sinon utilise la clé et la valeur fournies pour appeler make_pair sur les deux objets, puis effectue le même travail que la fonction d'insertion. C'est l'heure O (log (N)).
make_pair: fait un peu plus qu'une paire.
Il n'y avait aucun "besoin" d'ajouter de la place à la norme. En c ++ 11, je crois que le type de référence && a été ajouté. Cela a éliminé la nécessité d'une sémantique de déplacement et a permis l'optimisation d'un type spécifique de gestion de la mémoire. En particulier, la référence rvalue. L'opérateur d'insertion surchargé (value_type &&) ne tire pas parti de la sémantique in_place, et est donc beaucoup moins efficace. Bien qu'il offre la possibilité de traiter les références rvalue, il ignore leur objectif principal, qui est la construction d'objets en place.
la source
emplace()
est simplement la seule façon d'insérer un élément qui ne peut pas être copié ou déplacé. (& oui, peut-être, pour insérer le plus efficacement possible celui dont les constructeurs de copie et de déplacement coûtent beaucoup plus cher que la construction, si une telle chose existe) Il semble aussi que vous vous êtes trompé: il ne s'agit pas de " [profiter] de la référence rvalue pour utiliser les objets réels que vous avez déjà créés "; aucun objet n'est encore créé et vous transmettezmap
les arguments dont il a besoin pour le créer en lui-même. Vous ne créez pas l'objet.Outre les possibilités d'optimisation et la syntaxe plus simple, une distinction importante entre insertion et mise en place est que cette dernière permet des conversions explicites . (Ceci concerne toute la bibliothèque standard, pas seulement pour les cartes.)
Voici un exemple à démontrer:
#include <vector> struct foo { explicit foo(int); }; int main() { std::vector<foo> v; v.emplace(v.end(), 10); // Works //v.insert(v.end(), 10); // Error, not explicit v.insert(v.end(), foo(10)); // Also works }
Il s'agit certes d'un détail très spécifique, mais lorsque vous avez affaire à des chaînes de conversions définies par l'utilisateur, il vaut la peine de garder cela à l'esprit.
la source
v.emplace(v.end(), 10, 10);
... ou devriez-vous maintenant utiliserv.emplace(v.end(), foo(10, 10) );
:?emplace
utilisent une classe qui prend un seul paramètre. OMI, cela rendrait en fait la nature de la syntaxe variadique d'emplace beaucoup plus claire si plusieurs paramètres étaient utilisés dans les exemples.Le code suivant peut vous aider à comprendre «l'idée générale» de la
insert()
différence entreemplace()
:#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(); }
Le résultat que j'ai obtenu était:
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
Remarquerez que:
Un
unordered_map
stocke toujours en interne desFoo
objets (et non, disons,Foo *
s) sous forme de clés, qui sont toutes détruites lorsque leunordered_map
est détruit. Ici, lesunordered_map
touches internes de l 'étaient les foos 13, 11, 5, 10, 7 et 9.unordered_map
stocke réellement desstd::pair<const Foo, int>
objets, qui à leur tour stockent lesFoo
objets. Mais pour comprendre la «grande idée» de laemplace()
différence deinsert()
(voir l'encadré en surbrillance ci-dessous), il est normal d' imaginer temporairement cetstd::pair
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 cetstd::pair
objet intermédiaireunordered_map
introduit des aspects techniques subtils mais importants.L'insertion de chacun de
foo0
,foo1
etfoo2
nécessitait 2 appels à l'un desFoo
constructeurs de copie / déplacement de et 2 appels àFoo
destructeur de (comme je le décris maintenant):une. Insertion de chacun des
foo0
et afoo1
créé un objet temporaire (foo4
etfoo6
, respectivement) dont le destructeur a ensuite été immédiatement appelé après l'insertion terminée. De plus, lesFoo
s internes de unordered_map (qui sont lesFoo
s 5 et 7) ont également leurs destructeurs appelés lorsque le unordered_map a été détruit.b. Pour insérer
foo2
, nous avons d'abord créé explicitement un objet paire non temporaire (appelépair
), qui a appeléFoo
le constructeur de copie surfoo2
(création enfoo8
tant que membre interne depair
). Nous avons ensuiteinsert()
édité cette paire, ce qui a abouti à ununordered_map
nouvel appel du constructeur de copie (onfoo8
) pour créer sa propre copie interne (foo9
). Comme pourfoo
s 0 et 1, le résultat final était deux appels de destructeur pour cette insertion, la seule différence étant quefoo8
le destructeur n'était appelé que lorsque nous atteignions la fin demain()
plutôt que d'être appelé immédiatement après avoirinsert()
terminé.La
foo3
mise en place a donné lieu à un seul appel de constructeur de copie / déplacement (créationfoo10
interne dans leunordered_map
) et à un seul appel auFoo
destructeur de. (J'y reviendrai plus tard).Pour
foo11
, nous avons directement passé l'entier 11 àemplace(11, d)
afin d'unordered_map
appeler leFoo(int)
constructeur pendant que l'exécution est dans saemplace()
méthode. Contrairement à (2) et (3), nous n'avons même pas besoin d'unfoo
objet pré-existant pour le faire. Surtout, notez qu'un seul appel à unFoo
constructeur s'est produit (qui a crééfoo11
).Nous avons ensuite directement passé l'entier 12 à
insert({12, d})
. Contrairement àemplace(11, d)
(dont le rappel n'a abouti qu'à un seul appel à unFoo
constructeur), cet appel àinsert({12, d})
aboutit à deux appels àFoo
constructeur de (créationfoo12
etfoo13
).Cela montre quelle est la principale différence "dans son ensemble" entre
insert()
etemplace()
:Remarque: la raison du " presque " dans " presque toujours " ci-dessus est expliquée dans I) ci-dessous.
umap.emplace(foo3, d)
calledFoo
est la suivante: Puisque nous l'utilisonsemplace()
, le compilateur sait quefoo3
(unFoo
objet non-const ) est censé être un argument pour unFoo
constructeur. Dans ce cas, leFoo
constructeur le plus approprié est le constructeur de copie non constFoo(Foo& f2)
. C'est pourquoi onumap.emplace(foo3, d)
appelle un constructeur de copie alors que ceumap.emplace(11, d)
n'est pas le cas.Épilogue:
I. Notez qu'une surcharge de
insert()
est en fait équivalente àemplace()
. Comme décrit dans cette page cppreference.com , la surchargetemplate<class P> std::pair<iterator, bool> insert(P&& value)
(qui est overload (2)insert()
sur cette page cppreference.com) est équivalente àemplace(std::forward<P>(value))
.II. Où aller en partant d'ici?
une. Jouez avec le code source ci-dessus et étudiez la documentation
insert()
(par exemple ici ) etemplace()
(par exemple ici ) qui se trouve en ligne. Si vous utilisez un IDE tel qu'eclipse ou NetBeans, vous pouvez facilement demander à votre IDE de vous dire quelle surcharge deinsert()
ouemplace()
est appelée (dans eclipse, gardez simplement le curseur de votre souris sur l'appel de fonction pendant une seconde). Voici encore du code à essayer: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}}));
Vous verrez bientôt quelle surcharge du
std::pair
constructeur (voir référence ) finit par être utiliséeunordered_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. Voyez ce qui se passe lorsque vous utilisez une autre classe de conteneur (par exemple
std::set
oustd::unordered_multiset
) au lieu destd::unordered_map
.c. Maintenant, utilisez un
Goo
objet (juste une copie renommée deFoo
) au lieu de anint
comme type de plage dans anunordered_map
(c'est-à-dire utiliser à launordered_map<Foo, Goo>
place deunordered_map<Foo, int>
) et voyez combien et quelsGoo
constructeurs sont appelés. (Spoiler: il y a un effet mais ce n'est pas très dramatique.)la source
En termes de fonctionnalité ou de sortie, ils sont tous les deux identiques.
Pour les deux grandes mémoires, la mise en place d'objet est optimisée pour la mémoire et n'utilise pas de constructeurs de copie
Pour une explication simple et détaillée https://medium.com/@sandywits/all-about-emplace-in-c-71fd15e06e44
la source