web-dev-qa-db-fra.com

insérer vs emplace vs operator [] dans la carte c ++

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:

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

  2. 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?

159
German Capuano

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.

12
ChrisCM

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.

10
Kerrek SB

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:

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

    • Donc techniquement, notre _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.
  2. 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 Foos 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 foos 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().

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

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

  5. 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 objet Foo dans la portée de main() (suivie d'une copie ou d'un déplacement), si vous utilisez emplace() puis tout appel dans un constructeur Foo 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éthode emplace()). Le ou les arguments de la clé que vous transmettez à emplace() sont directement transmis à un appel du constructeur Foo 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 quitte emplace() 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.

  1. suite: La raison pour laquelle l'appel 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.)

9
Matthew K.