293 votes

insérer vs emplacer vs opérateur[] en c++ map

J'utilise des cartes pour la première fois et je me suis rendu compte qu'il y a plusieurs façons d'insérer un élément. Vous pouvez utiliser emplace() , operator[] o insert() et des variantes comme l'utilisation de value_type o make_pair . Bien qu'il y ait beaucoup d'informations sur chacun d'entre eux et des questions sur des cas particuliers, je n'arrive toujours pas à comprendre la situation dans son ensemble. Mes deux questions sont donc les suivantes :

  1. Quel est l'avantage de chacun d'entre eux par rapport aux autres ?

  2. Était-il nécessaire d'ajouter emplace à la norme ? Y a-t-il quelque chose qui n'était pas possible avant sans emplace ?

347voto

Dans le cas particulier d'une carte, les anciennes options n'étaient que deux : operator[] y insert (différentes saveurs de insert ). Je vais donc commencer à les expliquer.

El operator[] est un trouver ou ajouter opérateur. Il essaiera de trouver un élément avec la clé donnée dans la carte, et s'il existe, il retournera une référence à la valeur stockée. Si ce n'est pas le cas, il créera un nouvel élément inséré à la place avec une initialisation par défaut et retournera une référence à celui-ci.

El insert (dans le cas d'un élément unique) prend une fonction value_type ( std::pair<const Key,Value> ), il utilise la clé ( first ) et tente de l'insérer. Parce que std::map ne permet 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 en mesure de construire une version initialisée par défaut. valeur Il est donc inutilisable pour les types de valeurs qui ne peuvent pas être initialisés par défaut. La deuxième différence entre les deux est ce qui se passe lorsqu'il existe déjà un élément avec la clé donnée. Le site insert ne modifiera pas l'état de la carte, mais renverra plutôt un itérateur vers l'élément (et une fonction 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 passer n'importe quel objet à partir duquel la fonction value_type peuvent être construits, ce qui permet std::make_pair entre en jeu, puisqu'elle permet de créer simplement des 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 ils 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 à la insert fonction. Le site insert créera le nœud approprié dans l'arbre de recherche binaire, puis copiera l'adresse de l'utilisateur. value_type de l'argument au nœud. L'avantage d'utiliser value_type c'est que, eh bien, value_type toujours correspond à value_type vous ne pouvez pas vous tromper dans le type de l std::pair des arguments !

La différence se trouve dans [3]. La fonction std::make_pair est une fonction modèle qui va créer un std::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 pour le programme 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 pour être T==K,U==V donc l'appel à std::make_pair retournera un std::pair<K,V> (notez l'absence de const ). La signature nécessite value_type c'est-à-dire fermer mais pas la même que la valeur renvoyée par l'appel à la fonction std::make_pair . Comme il est suffisamment proche, il créera un temporaire du type correct et l'initialisera par copie. Il sera à son tour copié sur le nœud, créant ainsi un total de deux copies.

Cela peut être corrigé en fournissant les arguments du modèle :

m.insert( std::make_pair<const K,V>(t,u) );  // 4

Mais cela reste une source d'erreurs, tout comme le fait de taper explicitement le type dans le cas [1].

Jusqu'à présent, nous disposons de différentes manières d'appeler insert qui nécessitent la création de la value_type en externe et la copie de cet objet dans le conteneur. Vous pouvez également utiliser operator[] si le type est constructible par défaut y assignable (en se concentrant intentionnellement sur m[k]=v ), et il requiert l'initialisation par défaut d'un objet et de l'attribut copie de la valeur dans cet objet.

En C++11, avec les modèles variadiques et le transfert parfait, il existe une nouvelle façon d'ajouter des éléments dans un conteneur au moyen de la fonction mise en place de (création sur place). Le site emplace dans les différents conteneurs font fondamentalement la même chose : au lieu d'obtenir un fichier fuente à 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 plutôt des références à la t y u sont transmises à emplace qui les transmet au constructeur de l'objet value_type sous-objet à l'intérieur de la structure de données. Dans ce cas pas de des copies de la std::pair<const K,V> sont faites du tout, ce qui est l'avantage de emplace par rapport aux alternatives C++03. Comme dans le cas de insert il ne remplacera pas la valeur dans la carte.


Une question intéressante à laquelle je n'avais pas pensé est de savoir comment emplace peut en fait être implémentée pour une carte, et ce n'est pas un problème simple dans le cas général.

18voto

ChrisCM Points 3471

Emplacement : Tire profit 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é, ce qui est bon pour les objets de grande taille ! Temps O(log(N)).

Insert : possède des surcharges pour la référence standard lvalue et la référence rvalue, ainsi que des itérateurs vers des 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 à un temps contant, sinon c'est un 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 insert. Ceci est un temps O(log(N)).

faire_paire : Fait un peu plus que de faire une paire.

Il n'y avait pas de "n

16voto

Matthew K. Points 345

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.

  1. 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.
  1. 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é.
  2. 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.

  3. 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 ).

  4. 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).

12voto

Kerrek SB Points 194696

Outre les possibilités d'optimisation et la syntaxe plus simple, une distinction importante entre l'insertion et l'emplacement est que ce dernier permet explicite les conversions. (Ceci est valable pour toute la bibliothèque standard, pas seulement pour les cartes).

Voici un exemple pour le 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 est bon de le garder à l'esprit.

1voto

Daniel Langr Points 841

Il y a un problème supplémentaire qui n'a pas encore été abordé dans les autres réponses, et il s'applique pour std::map ainsi que pour std::unordered_map , std::set y std::unordered_set :

  • insert fonctionne avec un objet clé, ce qui implique qu'il n'a pas besoin d'allouer un nœud si la clé est déjà présente dans le conteneur.

  • emplace doit d'abord construire la clé, ce qui, généralement, nécessite l'allocation d'un nœud à chaque fois qu'il est appelé.

De ce point de vue, emplace peut être moins efficace que insert si la clé est déjà présente dans le conteneur. (Cela peut être significatif par exemple dans les applications multithreads avec des dictionnaires locaux aux threads, où les allocations doivent être synchronisées).

Démonstration en direct : https://godbolt.org/z/ornYcTqW9 . Notez qu'avec libstdc++ , emplace alloue 10 fois, tandis que insert une seule fois. Avec libc++ il n'y a qu'une seule allocation avec emplace également ; il semble qu'il y ait une certaine optimisation qui copie/déplace les clés * . J'ai obtenu le même résultat avec Microsoft STL Il semble donc qu'il manque une optimisation dans libstdc++. Cependant, le problème n'est peut-être pas uniquement lié aux conteneurs standard. Par exemple, concurrent_unordered_map de Intel/oneAPI TBB se comporte de la même manière que libstdc++ à cet égard.


* Notez que cette optimisation ne peut pas être appliquée dans les cas où les clés sont à la fois non copiable y inamovible . Dans cette démo en direct, nous avons 10 allocations même avec emplace et libc++ : https://godbolt.org/z/1b6b331qf . (Bien sûr, avec des clés non copiables et non mobiles, on ne peut pas utiliser insert ni try_emplace alors il n'y a pas d'autre option).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X