J'utilise des cartes pour la première fois et je me suis rendu compte qu'il y avait plusieurs façons d'insérer un élément. Vous pouvez utiliser emplace()
, operator[]
ou insert()
, ainsi que des variantes telles que 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 par rapport aux autres?
Est-il nécessaire d'ajouter de l'emploi à la norme? Y a-t-il quelque chose qui n'était pas possible auparavant sans cela?
Dans le cas particulier d'une carte, les anciennes options n'étaient que deux: operator[]
et insert
(différentes versions de insert
). Je vais donc commencer à expliquer ceux-ci.
operator[]
est un opérateur find-or-add . 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é avec l'initialisation par défaut et lui renverra une référence.
La fonction insert
(dans la version à élément unique) prend un value_type
(std::pair<const Key,Value>
), elle utilise la clé (first
member) et tente de l'insérer. Parce que std::map
n'autorise pas les doublons s'il existe un élément existant, il n'insère rien.
La première différence entre les deux est que operator[]
doit être capable de construire un initialisé par défaut value , et il est donc inutilisable pour les types de valeur qui ne peuvent pas être initialisés par défaut. La deuxième différence entre les deux est ce qui se passe quand il y a déjà un élément avec la clé donnée. La fonction insert
ne modifiera pas l'état de la carte, mais renverra un itérateur à l'élément (et un false
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 de value_type
, qui peut être créé de différentes manières. Vous pouvez le construire directement avec le type approprié ou transmettre tout objet à partir duquel le value_type
peut être construit. C’est là que std::make_pair
entre en jeu, car il permet la création simple de std::pair
objets , bien que ce ne soit 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 réalité équivalents. Dans les deux cas, le code crée un objet temporaire du même type (std::pair<const K,V>
) et le transmet à la fonction insert
. La fonction insert
crée le nœud approprié dans l'arborescence de recherche binaire, puis copie la partie value_type
de l'argument au nœud. L'avantage d'utiliser value_type
est que, bien, value_type
toujours correspond value_type
, vous ne pouvez pas saisir le type du std::pair
arguments!
La différence est dans [3]. La fonction std::make_pair
est une fonction modèle qui créera un std::pair
. La signature est:
template <typename T, typename U>
std::pair<T,U> make_pair(T const & t, U const & u );
J'ai intentionnellement pas fourni les arguments de modèle à std::make_pair
, car c'est l'usage commun. Et l'implication est que les arguments de modèle sont déduits de l'appel, dans ce cas comme T==K,U==V
, donc l'appel à std::make_pair
renverra un std::pair<K,V>
(notez le manquant const
). La signature nécessite value_type
c'est-à-dire close mais pas identique à la valeur renvoyée par l'appel à std::make_pair
. Parce qu'il est assez proche, il va créer un temporaire du type correct et l'initialiser par copie. Cela sera ensuite copié sur le nœud, créant un total de deux copies.
Cela peut être corrigé en fournissant les arguments du template:
m.insert( std::make_pair<const K,V>(t,u) ); // 4
Mais cela reste sujet aux erreurs de la même manière que de taper explicitement le type dans le cas [1].
Jusqu'à présent, nous avons différentes manières d'appeler insert
qui nécessitent la création du value_type
en externe et la copie de cet objet dans le conteneur. Sinon, vous pouvez utiliser operator[]
si le type est constructible par défaut et assignable (en vous concentrant intentionnellement uniquement dans m[k]=v
), et cela nécessite l'initialisation par défaut d'un objet et le copie de la valeur dans cet objet.
En C++ 11, avec des modèles variadiques et une transmission parfaite, il existe un nouveau moyen d’ajouter des éléments dans un conteneur à l’aide de emplacing (création sur place). Les fonctions emplace
dans les différents conteneurs font essentiellement la même chose: au lieu d’obtenir un source à partir duquel copie 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 transmis à emplace
, mais les références à l'objet t
et u
sont transmises à emplace
qui transmet les au constructeur du sous-objet value_type
à l'intérieur de la structure de données. Dans ce cas no des copies du std::pair<const K,V>
sont effectuées, ce qui représente l'avantage de emplace
par rapport aux alternatives C++ 03. Comme dans le cas de 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
peut réellement être implémenté pour une carte, et ce n'est pas un problème simple dans le cas général.
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.
Insert: comporte des surcharges pour la référence standard lvalue et la référence rvalue, ainsi que des itérateurs pour des listes d'éléments à insérer et des "astuces" quant à la position à laquelle un élément appartient. L'utilisation d'un itérateur "hint" peut ramener le temps d'insertion au temps contant, sinon c'est O(log(N)) temps.
Opérateur []: vérifie si l'objet existe et, le cas échéant, 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 O(log(N)) heure.
make_pair: ne fait guère plus que faire une paire.
Il n'y avait aucun "besoin" d'ajouter emplace à la norme. En c ++ 11, je pense que le type de référence && a été ajouté. Cela évitait la sémantique des déplacements et permettait d'optimiser un type spécifique de gestion de la mémoire. En particulier, la référence de valeur. L'opérateur surchargé d'insertion (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 de valeur, il ignore leur objectif principal, qui est la construction d'objets en place.
Outre les possibilités d'optimisation et la syntaxe plus simple, une distinction importante entre l'insertion et la mise en place est que cette dernière autorise les conversions explicites. (Ceci concerne toute la bibliothèque standard, pas seulement pour les cartes.)
Voici un exemple pour 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 traitez avec des chaînes de conversions définies par l’utilisateur, il convient de garder cela à l’esprit.
Le code suivant peut vous aider à comprendre la "grande idée" de la différence entre insert()
et emplace()
:
_#include <iostream>
#include <unordered_map>
#include <utility>
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;
std::cout << "\numap.insert(std::pair<Foo, int>(foo0, d))\n";
umap.insert(std::pair<Foo, int>(foo0, d));
//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)));
//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 les objets Foo
(et non, par exemple, _Foo *
_ s) sous forme de clés, qui sont toutes détruites lorsque le _unordered_map
_ est détruit. Ici, les clés internes de _unordered_map
_ étaient les suivantes: 13, 11, 5, 10, 7 et 9.
unordered_map
_ stocke en fait des objets _std::pair<const Foo, int>
_, qui stockent à leur tour les objets Foo
. Mais pour comprendre la "grande idée" de la différence entre emplace()
et insert()
(voir l'encadré surligné ci-dessous), il est correct de temporairement imaginez ce _std::pair
_ objet comme étant entièrement passif. Une fois que vous avez compris cette "idée globale", il est important de sauvegarder et de comprendre comment l'utilisation de cet objet intermédiaire _std::pair
_ par _unordered_map
_ introduit des détails techniques subtils mais importants.L'insertion de _foo0
_, _foo1
_ et _foo2
_ a nécessité 2 appels à l'un des constructeurs copier/déplacer de Foo
et 2 appels au destructeur de Foo
(comme je le décris maintenant):
une. L'insertion de _foo0
_ et _foo1
_ a créé un objet temporaire (_foo4
_ et _foo6
_, respectivement) dont le destructeur a ensuite été appelé immédiatement après la fin de l'insertion. En outre, les Foo
s internes de unordered_map (qui sont les foos 5 et 7) ont également eu leurs destructeurs appelés lorsque le désordre unordered_map a été détruit.
b. Pour insérer _foo2
_, nous avons plutôt d'abord explicitement créé un objet paire non temporaire (appelé pair
), appelé le constructeur de copie de Foo
sur _foo2
_ (créant _foo8
_ en tant que membre interne de pair
). Nous avons ensuite insert()
ed cette paire, ce qui a eu pour résultat que _unordered_map
_ a appelé à nouveau le constructeur de copie (sur _foo8
_) pour créer sa propre copie interne (_foo9
_). Comme avec foo
s 0 et 1, le résultat final était deux appels de destructeur pour cette insertion, la seule différence étant que le destructeur de _foo8
_ était appelé uniquement lorsque nous avons atteint la fin de main()
plutôt que d'être appelé immédiatement après la fin de insert()
.
La mise en place de _foo3
_ a donné lieu à un seul appel du constructeur de copie/déplacement (création de _foo10
_ en interne dans le _unordered_map
_) et à un seul appel au destructeur de Foo
. (J'y reviendrai plus tard).
Pour _foo11
_, nous avons directement passé l'entier 11 à emplace(11, d)
afin que _unordered_map
_ appelle le constructeur Foo(int)
pendant que l'exécution est dans sa méthode emplace()
. Contrairement à (2) et (3), nous n’avons même pas besoin d’un objet foo
antérieur à la sortie pour le faire. Il est important de noter qu’un seul appel à un constructeur Foo
a eu lieu.
Nous avons ensuite passé directement le nombre entier 12 à insert({12, d})
. Contrairement à emplace(11, d)
, cet appel à insert({12, d})
a abouti à deux appels au constructeur de Foo.
Cela montre quelle est la principale différence entre insert()
et emplace()
:
Considérant que l'utilisation de
insert()
presque toujours nécessite la construction ou l'existence d'un objetFoo
dans la portée demain()
(suivie d'une copie ou d'un déplacement), si vous utilisezemplace()
puis tout appel dans un constructeurFoo
se fait entièrement en interne dans _unordered_map
_ (c'est-à-dire à l'intérieur de la portée de la définition de la méthodeemplace()
). Le ou les arguments de la clé que vous transmettez àemplace()
sont directement transmis à un appel du constructeurFoo
au sein de _unordered_map
_ (détails supplémentaires facultatifs: l'endroit où cet objet nouvellement construit est immédiatement incorporé à l'une des variables membres de _unordered_map
_ de sorte qu'aucun destructeur n'est appelé lorsque l'exécution quitteemplace()
et qu'aucun constructeur de déplacement ou de copie n'est appelé).
Remarque: La raison de la presque dans presque toujours est expliquée dans I ) au dessous de.
umap.emplace(foo3, d)
appelé le constructeur de copie non-const de Foo
est la suivante: Comme nous utilisons emplace()
, le compilateur sait que _foo3
_ (un objet non-const Foo
) est censé être un argument pour Foo
constructeur. Dans ce cas, le constructeur le plus approprié Foo
est la construction de copie non constante 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.Épilogue:
I. Notez qu'une surcharge de insert()
est en fait équivalente à emplace()
. Comme décrit dans cette page cppreference.com , la surcharge template<class P> std::pair<iterator, bool> insert(P&& value)
(qui est surcharge (2) de 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 de insert()
(par exemple ici ) et de emplace()
(par exemple ici ) disponible en ligne. Si vous utilisez un IDE tel que Eclipse ou NetBeans, vous pouvez facilement obtenir votre IDE pour vous dire quelle surcharge de insert()
ou emplace()
est appelée (dans Eclipse, conservez votre le curseur de la souris reste immobile sur l'appel de fonction pendant une seconde). Voici un peu plus de 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 que la surcharge du constructeur _std::pair
_ (voir référence ) finit par être utilisée par _unordered_map
_ peut avoir un effet important sur le nombre d'objets copiés, déplacés , créé et/ou détruit et quand cela se produit.
b. Voyez ce qui se passe lorsque vous utilisez une autre classe de conteneur (par exemple, _std::set
_ ou _std::unordered_multiset
_) au lieu de _std::unordered_map
_.
c. Maintenant, utilisez un objet Goo
(juste une copie renommée de Foo
) au lieu de int
comme type de plage dans un _unordered_map
_ (c.-à-d. Utilisez _unordered_map<Foo, Goo>
_ au lieu de _unordered_map<Foo, int>
_) et voyez combien Les constructeurs Goo
sont appelés. (Spoiler: il y a un effet mais ce n'est pas très dramatique.)