Nelle mappe STL, è meglio usare map :: insert che []?


201

Qualche tempo fa, ho avuto una discussione con un collega su come inserire valori nelle mappe STL . Ho preferito map[key] = value; perché sembra naturale ed è chiaro da leggere mentre preferiva map.insert(std::make_pair(key, value))

Gliel'ho appena chiesto e nessuno dei due riesce a ricordare il motivo per cui l'inserto è migliore, ma sono sicuro che non fosse solo una preferenza di stile, bensì un motivo tecnico come l'efficienza. Il riferimento SGI STL dice semplicemente "A rigor di termini, questa funzione membro non è necessaria: esiste solo per comodità."

Qualcuno può dirmi quel motivo o sto solo sognando che ce n'è uno?


2
Grazie per tutte le ottime risposte - sono state davvero utili. Questa è una grande demo di overflow dello stack al suo meglio. Sono stato dilaniato su quale dovrebbe essere la risposta accettata: netjeff è più esplicito sul diverso comportamento, Greg Rogers ha menzionato i problemi di performance. Vorrei poter spuntare entrambi.
danio,

6
In realtà, con C ++ 11, probabilmente stai meglio usando map :: emplace che evita la doppia costruzione
einpoklum,

@einpoklum: In realtà, Scott Meyers suggerisce diversamente nel suo discorso "La ricerca in evoluzione di un C ++ efficace".
Thomas Eding,

3
@einpoklum: Questo è il caso quando si inserisce nella memoria di nuova costruzione. Ma a causa di alcuni requisiti standard per la mappa, ci sono ragioni tecniche per cui emplace può essere più lento dell'inserto. Il talk è disponibile gratuitamente su YouTube, come questo link youtube.com/watch?v=smqT9Io_bKo @ ~ 38-40 min mark. Per un collegamento Quindi, ecco stackoverflow.com/questions/26446352/...
Thomas Eding

1
Discuterei in realtà con alcune delle cose presentate da Meyers, ma questo va oltre lo scopo di questo thread di commenti e comunque, credo di dover ritirare il mio commento precedente.
einpoklum,

Risposte:


240

Quando scrivi

map[key] = value;

non c'è modo di sapere se hai sostituito il valuefor keyo se ne hai creato uno nuovo keycon value.

map::insert() creerà solo:

using std::cout; using std::endl;
typedef std::map<int, std::string> MyMap;
MyMap map;
// ...
std::pair<MyMap::iterator, bool> res = map.insert(MyMap::value_type(key,value));
if ( ! res.second ) {
    cout << "key " <<  key << " already exists "
         << " with value " << (res.first)->second << endl;
} else {
    cout << "created key " << key << " with value " << value << endl;
}

Per la maggior parte delle mie app, di solito non mi interessa se sto creando o sostituendo, quindi uso la più facile da leggere map[key] = value.


16
Va notato che map :: insert non sostituisce mai i valori. E nel caso generale direi che è meglio usare (res.first)->secondinvece che valueanche nel secondo caso.
dalle

1
Ho aggiornato per essere più chiaro che map :: insert non sostituisce mai. Ho lasciato il elseperché penso che l'uso valuesia più chiaro dell'iteratore. Solo se il tipo del valore avesse un ctor di copia insolito o op == sarebbe diverso e quel tipo causerebbe altri problemi usando i contenitori STL come map.
netjeff,

1
map.insert(std::make_pair(key,value))dovrebbe essere map.insert(MyMap::value_type(key,value)). Il tipo restituito da make_pairnon corrisponde al tipo preso da inserte la soluzione corrente richiede una conversione
David Rodríguez - dribeas

1
c'è un modo per dire se hai inserito o appena assegnato operator[], basta confrontare le dimensioni prima e dopo. Imho poter chiamare map::operator[]solo per tipi costruibili di default è molto più importante.
idclev 463035818,

@ DavidRodríguez-dribeas: buon suggerimento, ho aggiornato il codice nella mia risposta.
netjeff,

53

I due hanno una semantica diversa quando si tratta della chiave già esistente nella mappa. Quindi non sono realmente direttamente comparabili.

Ma la versione dell'operatore [] richiede di costruire il valore per impostazione predefinita e quindi di assegnarlo, quindi se questo è più costoso della copia della costruzione, allora sarà più costoso. A volte la costruzione di default non ha senso, e quindi sarebbe impossibile usare la versione dell'operatore [].


1
make_pair potrebbe richiedere un costruttore di copie, che sarebbe peggio di quello predefinito. +1 comunque.

1
La cosa principale è, come hai detto, che hanno una semantica diversa. Quindi nessuno dei due è migliore dell'altro, basta usare quello che fa quello che ti serve.
jalf

Perché il costruttore della copia dovrebbe essere peggiore del costruttore predefinito seguito dall'assegnazione? Se lo è, la persona che ha scritto la classe ha perso qualcosa, perché qualunque cosa faccia l'operatore = avrebbe dovuto fare lo stesso nel costruttore di copie.
Steve Jessop,

1
A volte la costruzione di default è costosa quanto l'assegnazione stessa. Naturalmente l'assegnazione e la costruzione della copia saranno equivalenti.
Greg Rogers,

@Arkadiy In una build ottimizzata, il compilatore rimuove spesso molte chiamate non necessarie al costruttore di copie.
antred

35

Un'altra cosa da notare con std::map:

myMap[nonExistingKey];creerà una nuova voce nella mappa, impostata su nonExistingKeyinizializzata su un valore predefinito.

Questo mi ha spaventato a morte la prima volta che l'ho visto (mentre sbattevo la testa contro un insetto brutto lascito). Non me lo sarei aspettato. Per me, sembra un'operazione get, e non mi aspettavo "l'effetto collaterale". Preferisci map.find()quando arrivi dalla tua mappa.


3
Questa è una visione decente, sebbene le mappe hash siano piuttosto universali per questo formato. Potrebbe essere una di quelle "stranezze che nessuno pensa che sia strano" proprio a causa della diffusione delle stesse convenzioni
Stephen J,

19

Se l'hit prestazionale del costruttore predefinito non è un problema, per favore, per amore di Dio, scegli la versione più leggibile.

:)


5
Secondo! Devo contrassegnare questo. Troppe persone si scambiano ottusità per accelerazioni di nano-secondo. Abbi pietà di noi povere anime che devono mantenere tali atrocità!
Mr.Ree,

6
Come ha scritto Greg Rogers: "I due hanno una semantica diversa quando si tratta della chiave già esistente nella mappa. Quindi non sono realmente direttamente comparabili".
dalle

Questa è una vecchia domanda e risposta. Ma la "versione più leggibile" è una ragione stupida. Perché ciò che è più leggibile dipende dalla persona.
Vallentin,

14

insert è migliore dal punto di vista della sicurezza.

L'espressione map[key] = valueè in realtà due operazioni:

  1. map[key] - creazione di un elemento della mappa con valore predefinito.
  2. = value - copiando il valore in quell'elemento.

Un'eccezione può verificarsi nel secondo passaggio. Di conseguenza l'operazione verrà eseguita solo parzialmente (un nuovo elemento è stato aggiunto alla mappa, ma quell'elemento non è stato inizializzato con value). La situazione in cui un'operazione non è completa, ma lo stato del sistema viene modificato, viene chiamata operazione con "effetto collaterale".

insertoperazione fornisce una forte garanzia, significa che non ha effetti collaterali ( https://en.wikipedia.org/wiki/Exception_safety ). insertè completamente completato o lascia la mappa in uno stato non modificato.

http://www.cplusplus.com/reference/map/map/insert/ :

Se deve essere inserito un singolo elemento, non ci sono cambiamenti nel contenitore in caso di eccezione (garanzia forte).


1
ancora più importante, inserire non richiede che il valore sia predefinito costruibile.
UmNyobe,

13

Se la tua applicazione è fondamentale per la velocità, ti consiglio di utilizzare l'operatore [] perché crea un totale di 3 copie dell'oggetto originale, di cui 2 sono oggetti temporanei e prima o poi distrutti come.

Ma in insert (), vengono create 4 copie dell'oggetto originale, di cui 3 sono oggetti temporanei (non necessariamente "temporanei") e vengono distrutti.

Ciò significa tempo extra per: 1. Allocazione di memoria di un oggetto 2. Chiamata di un costruttore supplementare 3. Chiamata di un distruttore supplementare 4. Deallocazione di memoria di un oggetto

Se i tuoi oggetti sono grandi, i costruttori sono tipici, i distruttori fanno molta liberazione delle risorse, i punti sopra contano ancora di più. Per quanto riguarda la leggibilità, penso che entrambi siano abbastanza equi.

Mi è venuta in mente la stessa domanda, ma non oltre la leggibilità ma la velocità. Ecco un codice di esempio attraverso il quale sono venuto a conoscenza del punto che ho citato.

class Sample
{
    static int _noOfObjects;

    int _objectNo;
public:
    Sample() :
        _objectNo( _noOfObjects++ )
    {
        std::cout<<"Inside default constructor of object "<<_objectNo<<std::endl;
    }

    Sample( const Sample& sample) :
    _objectNo( _noOfObjects++ )
    {
        std::cout<<"Inside copy constructor of object "<<_objectNo<<std::endl;
    }

    ~Sample()
    {
        std::cout<<"Destroying object "<<_objectNo<<std::endl;
    }
};
int Sample::_noOfObjects = 0;


int main(int argc, char* argv[])
{
    Sample sample;
    std::map<int,Sample> map;

    map.insert( std::make_pair<int,Sample>( 1, sample) );
    //map[1] = sample;
    return 0;
}

Uscita quando viene utilizzato insert () Uscita quando viene utilizzato l'operatore []


4
Ora esegui di nuovo quel test con le ottimizzazioni complete abilitate.
antred

2
Inoltre, considera cosa fa effettivamente l'operatore []. Per prima cosa cerca nella mappa una voce che corrisponde alla chiave specificata. Se ne trova uno, sovrascrive il valore di quella voce con quello specificato. In caso contrario, inserisce una nuova voce con la chiave e il valore specificati. Più grande è la tua mappa, più tempo impiegherà l'operatore [] a cercare la mappa. Ad un certo punto, questo non farà che compensare una copia aggiuntiva di c'tor (se questo rimane nel programma finale dopo che il compilatore ha fatto la sua magia di ottimizzazione).
antred

1
@antred, insertdeve fare la stessa ricerca, quindi nessuna differenza da quella [](perché le chiavi della mappa sono uniche).
Sz.

Bella illustrazione di ciò che sta succedendo con le stampe - ma cosa stanno realmente facendo questi 2 bit di codice? Perché sono necessarie 3 copie dell'oggetto originale?
rbennett485,

1. è necessario implementare l'operatore di assegnazione, altrimenti si otterranno numeri errati nel distruttore 2. utilizzare std :: move quando si costruisce la coppia per evitare un eccesso di costruzione della copia "map.insert (std :: make_pair <int, Sample> (1, std: : move (campione))); "
Fl0,

10

Ora in c ++ 11 penso che il modo migliore per inserire una coppia in una mappa STL sia:

typedef std::map<int, std::string> MyMap;
MyMap map;

auto& result = map.emplace(3,"Hello");

Il risultato sarà una coppia con:

  • Primo elemento (risultato prima), punta alla coppia inserita o punta alla coppia con questa chiave se la chiave esiste già.

  • Secondo elemento (risultato.secondo), vero se l'inserimento è corretto o falso qualcosa è andato storto.

PS: Se non fai caso sull'ordine puoi usare std :: unordered_map;)

Grazie!


9

Un gotcha con map :: insert () è che non sostituirà un valore se la chiave esiste già nella mappa. Ho visto il codice C ++ scritto dai programmatori Java in cui si aspettavano che insert () si comportasse allo stesso modo di Map.put () in Java, dove i valori vengono sostituiti.


2

Una nota è che puoi anche usare Boost.Assign :

using namespace std;
using namespace boost::assign; // bring 'map_list_of()' into scope

void something()
{
    map<int,int> my_map = map_list_of(1,2)(2,3)(3,4)(4,5)(5,6);
}

1

Ecco un altro esempio, che mostra che operator[] sovrascrive il valore per la chiave se esiste, ma .insert non sovrascrive il valore se esiste.

void mapTest()
{
  map<int,float> m;


  for( int i = 0 ; i  <=  2 ; i++ )
  {
    pair<map<int,float>::iterator,bool> result = m.insert( make_pair( 5, (float)i ) ) ;

    if( result.second )
      printf( "%d=>value %f successfully inserted as brand new value\n", result.first->first, result.first->second ) ;
    else
      printf( "! The map already contained %d=>value %f, nothing changed\n", result.first->first, result.first->second ) ;
  }

  puts( "All map values:" ) ;
  for( map<int,float>::iterator iter = m.begin() ; iter !=m.end() ; ++iter )
    printf( "%d=>%f\n", iter->first, iter->second ) ;

  /// now watch this.. 
  m[5]=900.f ; //using operator[] OVERWRITES map values
  puts( "All map values:" ) ;
  for( map<int,float>::iterator iter = m.begin() ; iter !=m.end() ; ++iter )
    printf( "%d=>%f\n", iter->first, iter->second ) ;

}

1

Questo è un caso piuttosto limitato, ma a giudicare dai commenti che ho ricevuto penso che valga la pena notare.

Ho visto persone in passato usare le mappe sotto forma di

map< const key, const val> Map;

per eludere i casi di sovrascrittura accidentale del valore, ma poi andare avanti scrivendo in alcuni altri bit di codice:

const_cast< T >Map[]=val;

La loro ragione per farlo, come ricordo, era perché erano sicuri che in questi certi frammenti di codice non avrebbero sovrascritto i valori delle mappe; quindi, andando avanti con il metodo più "leggibile" [].

In realtà non ho mai avuto problemi diretti con il codice che è stato scritto da queste persone, ma fino ad oggi mi sento fermamente convinto che i rischi - per quanto piccoli - non dovrebbero essere presi quando possono essere facilmente evitati.

Nei casi in cui hai a che fare con valori di mappe che non devono assolutamente essere sovrascritti, usa insert. Non fare eccezioni solo per la leggibilità.


Molte persone diverse l'hanno scritto? Sicuramente usare insert(non input), in quanto il const_castvalore precedente verrà sovrascritto, il che è molto non costante. Oppure, non contrassegnare il tipo di valore come const. (Questo genere di cose è di solito il risultato finale di const_cast, quindi è quasi sempre una bandiera rossa che indica un errore da qualche altra parte.)
Potatoswatter il

@Potatoswatter Hai ragione. Sto solo vedendo che const_cast [] viene utilizzato con i valori della mappa const da alcune persone quando sono sicuri che non sostituiranno un vecchio valore in alcuni bit di codice; poiché [] stesso è più leggibile. Come ho accennato nell'ultima parte della mia risposta, consiglierei di utilizzare insertnei casi in cui si desidera impedire la sovrascrittura dei valori. (Ho appena cambiato il inputin insert- grazie)
dk123 il

@Potatoswatter Se ricordo bene, i motivi principali per cui le persone sembrano utilizzare const_cast<T>(map[key])erano 1. [] è più leggibile, 2. sono fiduciosi in alcuni bit di codice che non sovrascriveranno valori e 3. non lo fanno vogliono altri bit di codice inconsapevole che sovrascrivano i loro valori - da qui il const value.
dk123,

2
Non ho mai sentito parlare di questo fatto; Dove l'hai visto? Scrivere const_castsembra più che negare l'ulteriore "leggibilità" di [], e quel tipo di fiducia è quasi motivo sufficiente per licenziare uno sviluppatore. Le difficili condizioni di runtime sono risolte da progetti a prova di proiettile, non da sentimenti viscerali.
Potatoswatter,

@Potatoswatter Ricordo che era durante uno dei miei precedenti lavori nello sviluppo di giochi educativi. Non potrei mai convincere le persone a scrivere il codice per cambiare le loro abitudini. Hai assolutamente ragione e sono fortemente d'accordo con te. Dai tuoi commenti, ho deciso che probabilmente vale la pena notare questo rispetto alla mia risposta originale e quindi l'ho aggiornato per riflettere questo. Grazie!
dk123,

1

Il fatto che la funzione std :: map insert()non sovrascriva il valore associato alla chiave ci consente di scrivere codice di enumerazione oggetti come questo:

string word;
map<string, size_t> dict;
while(getline(cin, word)) {
    dict.insert(make_pair(word, dict.size()));
}

È un problema piuttosto comune quando è necessario mappare diversi oggetti non univoci su alcuni ID nell'intervallo 0..N. Tali ID possono essere successivamente utilizzati, ad esempio, negli algoritmi grafici. Alternativa con operator[]sarebbe meno leggibile secondo me:

string word;
map<string, size_t> dict;
while(getline(cin, word)) {
    size_t sz = dict.size();
    if (!dict.count(word))
        dict[word] = sz; 
} 
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.