insert vs emplace vs operator [] dans la carte c ++


207

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_typeou 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:

  1. Quel est l'avantage de chacun d'eux sur les autres?

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


1
La sémantique des emplacements permet des conversions explicites et une initialisation directe.
Kerrek SB

3
Maintenant operator[]est basé sur try_emplace. Cela vaut peut-être la peine de le mentionner insert_or_assign.
FrankHB

@FrankHB si vous (ou quelqu'un d'autre) ajoutez une réponse à jour, je pourrais changer celle acceptée.
German Capuano

Réponses:


243

Dans le cas particulier d'une carte, les anciennes options n'étaient que deux: operator[]et insert(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 insertfonction (dans la saveur d'élément unique) prend un value_type( std::pair<const Key,Value>), elle utilise la clé ( firstmembre) et essaie de l'insérer. Parce std::mapque 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. La insertfonction ne modifiera pas l'état de la carte, mais retournera à la place un itérateur à l'élément (et un falseindiquant 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 insertl'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 le value_typepeut être construit, ce qui std::make_pairentre en jeu, car il permet de créer simplement des std::pairobjets, 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 à la insertfonction. La insertfonction créera le nœud approprié dans l'arborescence de recherche binaire, puis copiera la value_typepartie de l'argument vers le nœud. L'avantage de l'utilisation value_typeest que, eh bien, correspondvalue_type toujours , vous ne pouvez pas mal saisir le type de value_typestd::pair arguments!

La différence est dans [3]. La fonction std::make_pairest une fonction modèle qui créera un fichier 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 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 être T==K,U==V, donc l'appel à std::make_pairretournera un std::pair<K,V>(notez le manquant const). La signature exige value_typeque 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 insertqui nécessitent la création de l' value_typeexterne et la copie de cet objet dans le conteneur. Vous pouvez également utiliser operator[]si le type est constructible et assignable par défaut (se concentrant intentionnellement uniquement dans m[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 emplacefonctions 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' objet tet usont passées à ce emplacequi les transmet au constructeur du value_typesous - objet à l'intérieur de la structure de données. Dans ce cas, aucune copie de std::pair<const K,V>n'est effectuée, ce qui est l'avantage par emplacerapport aux alternatives C ++ 03. Comme dans le cas, insertil ne remplacera pas la valeur de la carte.


Une question intéressante à laquelle je n'avais pas pensé est de savoir comment emplacemettre en œuvre une carte, et ce n'est pas un problème simple dans le cas général.


6
Ceci est suggéré dans la réponse, mais map [] = val écrasera la valeur précédente s'il en existe une.
dk123

une question plus intéressante à mon sens, c'est qu'elle ne sert pas à grand-chose. Parce que vous enregistrez la copie de paire, ce qui est bien car aucune copie de paire ne signifie pas de mapped_typecopie séparée. Ce que nous voulons, c'est mettre en place la construction du mapped_typedans la paire, et mettre en place la construction de la paire dans la carte. Par conséquent, la std::pair::emplacefonction et sa prise en charge de transfert dans map::emplacesont 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.
v.oddou

En fait, je modifie ce commentaire, en C ++ 11, il y a un constructeur de paires de modèles qui sert exactement le même objectif que mettre en place dans le cas d'une construction à 1 argument. et une construction étrange par morceaux, comme ils l'appellent, utilisant des tuples pour transférer des arguments, donc nous pouvons toujours avoir une transmission parfaite, semble-t-il.
v.oddou

On dirait qu'il y a un bug de performance d'insertion dans unordered_map et map: lien
Deqing

2
Cela pourrait être bien de mettre à jour cela avec des informations sur insert_or_assignet try_emplace(tous deux à partir de C ++ 17), qui aident à combler certaines lacunes dans les fonctionnalités des méthodes existantes.
ShadowRanger

16

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.


4
" Il n'y avait aucun" besoin "d'ajouter de la place à la norme." C'est manifestement faux. 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 transmettez maples arguments dont il a besoin pour le créer en lui-même. Vous ne créez pas l'objet.
underscore_d

10

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.


Imaginez que foo ait besoin de deux entiers dans son cteur plutôt que d'un. Pourriez-vous utiliser cet appel? v.emplace(v.end(), 10, 10); ... ou devriez-vous maintenant utiliser v.emplace(v.end(), foo(10, 10) ); :?
Kaitain

Je n'ai pas accès à un compilateur pour le moment, mais je suppose que cela signifie que les deux versions fonctionneront. Presque tous les exemples que vous voyez emplaceutilisent 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.
Kaitain

10

Le code suivant peut vous aider à comprendre «l'idée générale» de la insert()différence entre 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();
}

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:

  1. Un unordered_map stocke toujours en interne des Fooobjets (et non, disons, Foo *s) sous forme de clés, qui sont toutes détruites lorsque le unordered_mapest détruit. Ici, les unordered_maptouches internes de l 'étaient les foos 13, 11, 5, 10, 7 et 9.

    • Donc, techniquement, notre unordered_mapstocke réellement des std::pair<const Foo, int>objets, qui à leur tour stockent les Fooobjets. Mais pour comprendre la «grande idée» de la emplace()différence de insert()(voir l'encadré en surbrillance ci-dessous), il est normal d' imaginer temporairement cet std::pairobjet 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 std::pairobjet intermédiaire unordered_mapintroduit des aspects techniques subtils mais importants.
  2. L'insertion de chacun de foo0, foo1et foo2nécessitait 2 appels à l'un des Fooconstructeurs de copie / déplacement de et 2 appels àFoo destructeur de (comme je le décris maintenant):

    une. Insertion de chacun desfoo0 et a foo1créé un objet temporaire ( foo4et foo6, respectivement) dont le destructeur a ensuite été immédiatement appelé après l'insertion terminée. De plus, les Foos internes de unordered_map (qui sont les Foos 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é Foole constructeur de copie sur foo2(création en foo8tant que membre interne de pair). Nous avons ensuite insert()édité cette paire, ce qui a abouti à un unordered_mapnouvel appel du constructeur de copie (on foo8) pour créer sa propre copie interne (foo9 ). Comme pour foos 0 et 1, le résultat final était deux appels de destructeur pour cette insertion, la seule différence étant que foo8le destructeur n'était appelé que lorsque nous atteignions la fin de main()plutôt que d'être appelé immédiatement après avoir insert()terminé.

  3. La foo3mise en place a donné lieu à un seul appel de constructeur de copie / déplacement (création foo10interne dans le unordered_map) et à un seul appel au Foodestructeur de. (J'y reviendrai plus tard).

  4. Pour foo11, nous avons directement passé l'entier 11 à emplace(11, d)afin d' unordered_mapappeler le Foo(int)constructeur pendant que l'exécution est dans sa emplace()méthode. Contrairement à (2) et (3), nous n'avons même pas besoin d'un fooobjet pré-existant pour le faire. Surtout, notez qu'un seul appel à un Fooconstructeur s'est produit (qui a créé foo11).

  5. 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 à un Fooconstructeur), cet appel à insert({12, d})aboutit à deux appels àFoo constructeur de (création foo12et foo13).

Cela montre quelle est la principale différence "dans son ensemble" entre insert() et emplace():

Alors que l'utilisation nécessite insert() presque toujours la construction ou l'existence d'un Fooobjet dans main()la portée de s (suivie d'une copie ou d'un déplacement), si l'utilisation de emplace()tout appel à un Fooconstructeur se fait entièrement en interne dans le unordered_map(c'est-à-dire à l'intérieur de la portée de la emplace()définition de la méthode). Le ou les arguments de la clé à laquelle vous passez emplace()sont directement transmis à un Fooappel de constructeur dans unordered_map::emplace()la définition de (détails supplémentaires facultatifs: où cet objet nouvellement construit est immédiatement incorporé dans l'une des unordered_mapvariables membres de de sorte qu'aucun destructeur ne soit appelé lorsque l'exécution quitte emplace()et aucun constructeur de déplacement ou de copie n'est appelé).

Remarque: la raison du " presque " dans " presque toujours " ci-dessus est expliquée dans I) ci-dessous.

  1. suite: La raison pour laquelle l'appel du constructeur de copie non-const de umap.emplace(foo3, d)called Fooest la suivante: Puisque nous l'utilisons emplace(), le compilateur sait que foo3(un Fooobjet non-const ) est censé être un argument pour un Fooconstructeur. Dans ce cas, le Fooconstructeur le plus approprié est le constructeur de copie non const Foo(Foo& f2). C'est pourquoi on umap.emplace(foo3, d)appelle un constructeur de copie alors que ce umap.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 surcharge template<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 ) et emplace()(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 de insert()ou emplace()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::pairconstructeur (voir référence ) finit par être utilisée unordered_mappeut 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::setou std::unordered_multiset) au lieu destd::unordered_map .

c. Maintenant, utilisez un Gooobjet (juste une copie renommée de Foo) au lieu de an intcomme type de plage dans an unordered_map(c'est-à-dire utiliser à la unordered_map<Foo, Goo>place de unordered_map<Foo, int>) et voyez combien et quels Gooconstructeurs sont appelés. (Spoiler: il y a un effet mais ce n'est pas très dramatique.)


En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.