inserisci vs emplace vs operator [] nella mappa c ++


193

Sto usando le mappe per la prima volta e mi sono reso conto che ci sono molti modi per inserire un elemento. Puoi usare emplace(), operator[]o insert(), oltre a varianti come usare value_typeo make_pair. Mentre ci sono molte informazioni su tutti loro e domande su casi particolari, non riesco ancora a capire il quadro generale. Quindi, le mie due domande sono:

  1. Qual è il vantaggio di ciascuno di essi rispetto agli altri?

  2. C'è stato bisogno di aggiungere emplace allo standard? C'è qualcosa che prima non era possibile senza di essa?


1
La semantica della postazione consente conversioni esplicite e inizializzazione diretta.
Kerrek SB,

3
Ora operator[]si basa su try_emplace. Vale la pena menzionare insert_or_assignanche questo.
FrankHB il

@FrankHB se tu (o qualcun altro) aggiungi una risposta aggiornata, potrei cambiare quella accettata.
Capuano tedesco,

Risposte:


230

Nel caso particolare di una mappa le vecchie opzioni erano solo due: operator[]e insert(diversi gusti di insert). Quindi inizierò a spiegarli.

Il operator[]è una scoperta-o-add dell'operatore. Tenterà di trovare un elemento con la chiave specificata all'interno della mappa e, se esiste, restituirà un riferimento al valore memorizzato. In caso contrario, creerà un nuovo elemento inserito in posizione con l'inizializzazione predefinita e restituirà un riferimento ad esso.

La insertfunzione (nel sapore di un singolo elemento) accetta un value_type( std::pair<const Key,Value>), usa la chiave ( firstmembro) e prova a inserirla. Poiché std::mapnon consente duplicati se esiste un elemento esistente, non inserirà nulla.

La prima differenza tra i due è che operator[]deve essere in grado di costruire un predefinito inizializzata valore , ed è quindi inutilizzabile per tipi di valore che non può essere predefinito inizializzata. La seconda differenza tra i due è ciò che accade quando esiste già un elemento con la chiave specificata. La insertfunzione non modificherà lo stato della mappa, ma restituirà invece un iteratore all'elemento (e falseindica che non è stato inserito).

// 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

Nel caso insertdell'argomento è un oggetto di value_type, che può essere creato in diversi modi. Puoi costruirlo direttamente con il tipo appropriato o passare qualsiasi oggetto da cui value_typepuò essere costruito, che è dove std::make_pairentra in gioco, in quanto consente una semplice creazione di std::pairoggetti, anche se probabilmente non è quello che vuoi ...

L'effetto netto delle seguenti chiamate è simile :

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

Ma non sono proprio gli stessi ... [1] e [2] sono in realtà equivalenti. In entrambi i casi il codice crea un oggetto temporaneo dello stesso tipo ( std::pair<const K,V>) e lo passa alla insertfunzione. La insertfunzione creerà il nodo appropriato nella struttura di ricerca binaria e quindi copierà la value_typeparte dall'argomento nel nodo. Il vantaggio dell'uso value_typeè che, beh, corrispondevalue_type sempre , non è possibile sbagliare a digitare il tipo di argomenti! value_typestd::pair

La differenza è in [3]. La funzione std::make_pairè una funzione modello che creerà un std::pair. La firma è:

template <typename T, typename U>
std::pair<T,U> make_pair(T const & t, U const & u );

Non ho intenzionalmente fornito gli argomenti del modello std::make_pair, in quanto questo è l'uso comune. E l'implicazione è che gli argomenti del modello sono dedotti dalla chiamata, in questo caso T==K,U==V, quindi la chiamata a std::make_pairrestituirà un std::pair<K,V>(notare il mancante const). La firma richiede value_typeche sia vicino ma non uguale al valore restituito dalla chiamata a std::make_pair. Poiché è abbastanza vicino, creerà un temporaneo del tipo corretto e la copierà inizializzandolo. Questo a sua volta verrà copiato sul nodo, creando un totale di due copie.

Questo può essere risolto fornendo gli argomenti del modello:

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

Ma questo è ancora soggetto a errori nello stesso modo in cui si digita esplicitamente il tipo nel caso [1].

Fino a questo punto, abbiamo diversi modi di chiamare insertche richiedono la creazione value_typedell'esterno e la copia di quell'oggetto nel contenitore. In alternativa, è possibile utilizzare operator[]se il tipo è predefinito costruibile e assegnabile (si concentra solo intenzionalmente su m[k]=v) e richiede l'inizializzazione predefinita di un oggetto e la copia del valore in quell'oggetto.

In C ++ 11, con i modelli variadici e l'inoltro perfetto c'è un nuovo modo di aggiungere elementi in un contenitore mediante la collocazione (creazione sul posto). Le emplacefunzioni nei diversi contenitori fanno fondamentalmente la stessa cosa: invece di ottenere una sorgente da cui copiare nel contenitore, la funzione accetta i parametri che sarà trasmessa al costruttore dell'oggetto memorizzato nel contenitore.

m.emplace(t,u);               // 5

In [5], std::pair<const K, V>non viene creato e passato emplace, ma piuttosto vengono passati riferimenti a te uoggetto emplaceche li inoltra al costruttore dell'oggetto value_typesecondario all'interno della struttura dei dati. In questo caso nonstd::pair<const K,V> viene eseguita alcuna copia di ciò, il che è il vantaggio delle emplacealternative C ++ 03. Come nel caso in insertcui non sovrascriverà il valore nella mappa.


Una domanda interessante a cui non avevo pensato è come emplacepuò essere effettivamente implementata per una mappa, e questo non è un problema semplice nel caso generale.


5
Questo è suggerito nella risposta, ma map [] = val sovrascriverà il valore precedente se ne esiste uno.
dk123,

una domanda più interessante nel mio senso è che serve a poco. Perché si salva la copia di coppia, il che è positivo perché nessuna copia di coppia significa nessuna mapped_typecopia di copia. Ciò che vogliamo è inserire la costruzione della mapped_typecoppia nella coppia e posizionare la costruzione della coppia nella mappa. Pertanto, la std::pair::emplacefunzione e il relativo supporto di inoltro map::emplacesono entrambi mancanti. Nella sua forma attuale, devi ancora dare un mapped_type costruito al costruttore della coppia che lo copierà una volta. è meglio di due volte, ma ancora non va bene.
v.oddou,

in realtà modifico quel commento, in C ++ 11 c'è un costruttore di coppie di template che ha lo stesso identico scopo di emplace nel caso della costruzione di 1 argomento. e qualche strano costrutto a tratti, come lo chiamano, usando le tuple per inoltrare argomenti, quindi possiamo ancora avere un perfetto inoltro.
v.oddou,

Sembra che ci sia un errore nelle prestazioni di inserimento in unordered_map e map: link
Deqing

1
Potrebbe essere utile aggiornarlo con le informazioni su insert_or_assigne try_emplace(entrambe da C ++ 17), che aiutano a colmare alcune lacune nelle funzionalità rispetto ai metodi esistenti.
ShadowRanger,

15

Luogo: sfrutta il riferimento al valore per usare gli oggetti reali che hai già creato. Ciò significa che non viene chiamato nessun costruttore di copia o spostamento, ottimo per oggetti GRANDI! O (log (N)) tempo.

Inserisci: presenta sovraccarichi per riferimento lvalore standard e riferimento rvalore, nonché iteratori di elenchi di elementi da inserire e "suggerimenti" sulla posizione a cui appartiene un elemento. L'uso di un iteratore "suggerimento" può portare il tempo impiegato dall'inserimento in contante, altrimenti è O (log (N)).

Operatore []: verifica se l'oggetto esiste e, in tal caso, modifica il riferimento a questo oggetto, altrimenti utilizza la chiave e il valore forniti per chiamare make_pair sui due oggetti, quindi svolge la stessa funzione della funzione di inserimento. Questo è il tempo O (log (N)).

make_pair: fa poco più che creare una coppia.

Non c'era "necessità" per aggiungere emplace allo standard. In c ++ 11 credo che sia stato aggiunto il tipo di riferimento &&. Ciò ha rimosso la necessità di spostare la semantica e ha consentito l'ottimizzazione di alcuni tipi specifici di gestione della memoria. In particolare, il riferimento al valore. L'operatore insert (value_type &&) sovraccaricato non sfrutta la semantica in_place ed è quindi molto meno efficiente. Mentre fornisce la capacità di gestire i riferimenti rvalue, ignora il loro scopo chiave, che è in atto la costruzione di oggetti.


4
" Non c'era" necessità "per aggiungere emplace allo standard." Questo è palesemente falso. emplace()è semplicemente l'unico modo per inserire un elemento che non può essere copiato o spostato. (E sì, forse, per inserire in modo più efficiente uno i cui costruttori di copia e spostamento costano molto più della costruzione, se esiste una cosa del genere) Sembra anche che tu abbia sbagliato l'idea: non si tratta di " [trarre vantaggio] dal riferimento al valore per utilizzare gli oggetti reali che hai già creato "; nessun oggetto è ancora creata, e si porta avanti la mapgli argomenti che ha bisogno di creare dentro se stesso. Non fai l'oggetto.
underscore_d

10

Oltre alle opportunità di ottimizzazione e alla sintassi più semplice, un'importante distinzione tra inserimento e collocazione è che quest'ultima consente conversioni esplicite . (Questo è nell'intera libreria standard, non solo per le mappe.)

Ecco un esempio per dimostrare:

#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
}

Questo è certamente un dettaglio molto specifico, ma quando hai a che fare con catene di conversioni definite dall'utente, vale la pena tenerlo a mente.


Immagina che Foo abbia richiesto due ints nel suo ctor anziché uno. Saresti in grado di utilizzare questa chiamata? v.emplace(v.end(), 10, 10); ... o ora dovresti usare v.emplace(v.end(), foo(10, 10) ); :?
Kaitain,

Non ho accesso a un compilatore in questo momento, ma presumo che ciò significhi che entrambe le versioni funzioneranno. Quasi tutti gli esempi che vedi per emplaceutilizzare una classe che accetta un singolo parametro. IMO renderebbe molto più chiara la natura della sintassi variadica di emplace se negli esempi venissero utilizzati più parametri.
Kaitain,

9

Il seguente codice può aiutarti a capire l '"idea generale" di come insert()differisce da 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();
}

L'output che ho ottenuto è stato:

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

Notare che:

  1. Un oggetto unordered_mapmemorizza sempre internamente Foooggetti (e non, diciamo, Foo *) come chiavi, che vengono tutti distrutti quando unordered_mapvengono distrutti. Qui, le unordered_mapchiavi interne erano 13, 11, 5, 10, 7 e 9 di Foos.

    • Quindi tecnicamente, il nostro unordered_mapeffettivamente immagazzina std::pair<const Foo, int>oggetti, che a loro volta immagazzinano gli Foooggetti. Ma per capire la "grande idea" di come emplace()differisce insert()(vedi riquadro evidenziato sotto), va bene immaginare temporaneamente questo std::pairoggetto come completamente passivo. Una volta compresa questa "idea generale", è importante eseguire il backup e comprendere in che modo l'uso di questo std::pairoggetto intermedio unordered_mapintroduce tecnicismi sottili ma importanti.
  2. Inserendo ciascuno di foo0, foo1e foo2richiesto 2 chiamate a uno dei Foocostruttori di copia / spostamento e 2 chiamate al Foodistruttore (come descriverò ora):

    un. Inserendo ciascuno di foo0e foo1creato un oggetto temporaneo ( foo4e foo6, rispettivamente) il cui distruttore veniva quindi immediatamente chiamato dopo l'inserimento completato. Inoltre, anche le Foos interne di unordered_map (che sono Foos 5 e 7) sono state chiamate ai loro distruttori quando la unordered_map è stata distrutta.

    b. Per inserire foo2, invece, abbiamo prima creato esplicitamente un oggetto coppia non temporaneo (chiamato pair), che ha chiamato Fooil costruttore della copia su foo2(creando foo8come membro interno di pair). Abbiamo quindi modificato insert()questa coppia, che ha portato alla unordered_mapchiamata del costruttore di copie (on foo8) per creare la propria copia interna ( foo9). Come per foos 0 e 1, il risultato finale sono state due chiamate al distruttore per questo inserimento, con l'unica differenza che foo8il distruttore veniva chiamato solo quando abbiamo raggiunto la fine main()anziché essere chiamato immediatamente dopo aver insert()terminato.

  3. L'implementazione ha foo3comportato solo 1 chiamata del costruttore copia / sposta (creando foo10internamente nel unordered_map) e solo 1 chiamata al Foodistruttore. (Tornerò su questo più tardi).

  4. Per foo11, abbiamo passato direttamente l'intero 11 a in emplace(11, d)modo che unordered_mapchiamerebbe il Foo(int)costruttore mentre l'esecuzione è nel suo emplace()metodo. A differenza di (2) e (3), non abbiamo nemmeno bisogno di qualche foooggetto pre-uscita per farlo. È importante notare che si è verificata solo 1 chiamata a un Foocostruttore (che ha creato foo11).

  5. Abbiamo quindi passato direttamente l'intero 12 a insert({12, d}). Diversamente da emplace(11, d)(il cui richiamo ha comportato solo 1 chiamata a un Foocostruttore), questa chiamata ha insert({12, d})comportato due chiamate al Foocostruttore (creazione foo12e foo13).

Questo mostra quale sia la principale differenza "grande quadro" tra insert()ed emplace()è:

Considerando che l'utilizzo insert() quasi sempre richiede la costruzione o l'esistenza di un Foooggetto main()nell'ambito (seguito da una copia o da uno spostamento), se si utilizza emplace()quindi qualsiasi chiamata a un Foocostruttore viene eseguita interamente internamente unordered_map(ovvero all'interno dell'ambito della emplace()definizione del metodo). Gli argomenti per la chiave a cui si passa emplace()vengono inoltrati direttamente a una Foochiamata del costruttore all'interno unordered_map::emplace()della definizione (dettagli aggiuntivi opzionali: dove questo oggetto di nuova costruzione viene immediatamente incorporato in una delle unordered_mapvariabili membro in modo che nessun distruttore venga chiamato quando l'esecuzione viene interrotta emplace()e non vengono chiamati costruttori di spostamento o copia).

Nota: il motivo del " quasi " in " quasi sempre " sopra è spiegato in I) di seguito.

  1. continua: Il motivo per cui chiamare il costruttore di copie non const umap.emplace(foo3, d)chiamato Fooè il seguente: Dal momento che stiamo usando emplace(), il compilatore sa che foo3(un Foooggetto non const ) è pensato per essere un argomento per qualche Foocostruttore. In questo caso, il Foocostruttore più adatto è il costruttore della copia non const Foo(Foo& f2). Questo è il motivo per cui ha umap.emplace(foo3, d)chiamato un costruttore di copie mentre umap.emplace(11, d)non lo ha fatto.

Epilogo:

I. Si noti che un sovraccarico di insert()è effettivamente equivalente a emplace() . Come descritto in questa pagina di cppreference.com , il sovraccarico template<class P> std::pair<iterator, bool> insert(P&& value)(che è il sovraccarico (2) di insert()in questa pagina di cppreference.com) è equivalente a emplace(std::forward<P>(value)).

II. Dove andare da qui?

un. Gioca con il codice sorgente sopra e studia la documentazione per insert()(ad es. Qui ) e emplace()(ad es. Qui ) che si trova online. Se stai usando un IDE come eclipse o NetBeans, puoi facilmente far sì che il tuo IDE ti dica quale sovraccarico di insert()o emplace()viene chiamato (in eclipse, mantieni il cursore del mouse costante sulla chiamata di funzione per un secondo). Ecco qualche altro codice da provare:

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}}));

Vedrai presto che quale sovraccarico del std::paircostruttore (vedi riferimento ) finisce per essere utilizzato unordered_mappuò avere un effetto importante su quanti oggetti vengono copiati, spostati, creati e / o distrutti, nonché quando tutto ciò si verifica.

b. Guarda cosa succede quando usi qualche altra classe di container (es. std::setO std::unordered_multiset) invece di std::unordered_map.

c. Ora usa un Goooggetto (solo una copia rinominata di Foo) anziché un inttipo di intervallo in un unordered_map(cioè usa unordered_map<Foo, Goo>invece di unordered_map<Foo, int>) e vedi quanti e quali Goocostruttori sono chiamati. (Spoiler: c'è un effetto ma non è molto drammatico.)


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.