Qual è il modo preferito / idiomatico per inserire in una mappa?


113

Ho identificato quattro diversi modi per inserire elementi in un std::map:

std::map<int, int> function;

function[0] = 42;
function.insert(std::map<int, int>::value_type(0, 42));
function.insert(std::pair<int, int>(0, 42));
function.insert(std::make_pair(0, 42));

Quale di questi è il modo preferito / idiomatico? (E c'è un altro modo a cui non ho pensato?)


26
La tua mappa dovrebbe chiamarsi "risposte", non "funzione"
Vincent Robert

2
@ Vincent: Hm? Una funzione è fondamentalmente una mappa tra due insiemi.
fredoverflow

7
@FredOverflow: sembra che il commento di Vincent sia una specie di scherzo su certi libri ...
Victor Sorokin,

1
Sembra contraddire l'originale: 42 non può essere contemporaneamente la risposta a (a) la vita, l'universo e tutto, e (b) niente. Ma allora come esprimi la vita, l'universo e tutto il resto come un int?
Stuart Golodetz

19
@sgolodetz Puoi esprimere tutto con un int abbastanza grande.
Yakov Galka

Risposte:


90

Prima di tutto, operator[] e le insertfunzioni membro non sono funzionalmente equivalenti:

  • La operator[]si cerca per la chiave, inserire un valore predefinito costruito valore se non trovato, e restituire un riferimento a cui si assegna un valore. Ovviamente, questo può essere inefficiente se mapped_typepuò trarre vantaggio dall'essere inizializzato direttamente invece di essere costruito e assegnato di default. Questo metodo inoltre rende impossibile determinare se un inserimento è effettivamente avvenuto o se hai solo sovrascritto il valore di una chiave inserita in precedenza
  • La insertfunzione membro non avrà effetto se la chiave è già presente nella mappa e, sebbene venga spesso dimenticata, restituisce unstd::pair<iterator, bool> che può essere di interesse (in particolare per determinare se l'inserimento è stato effettivamente eseguito).

Da tutte le possibilità elencate per chiamare insert, tutte e tre sono quasi equivalenti. Come promemoria, diamo un'occhiata alla insertfirma nello standard:

typedef pair<const Key, T> value_type;

  /* ... */

pair<iterator, bool> insert(const value_type& x);

Allora in cosa differiscono le tre chiamate?

  • std::make_pairsi basa sulla deduzione dell'argomento del modello e potrebbe (e in questo caso produrrà ) produrre qualcosa di un tipo diverso dall'effettivo value_typedella mappa, che richiederà una chiamata aggiuntiva al std::paircostruttore del modello per convertirlo in value_type(es .: aggiunta consta first_type)
  • std::pair<int, int>richiederà anche una chiamata aggiuntiva al costruttore del modello std::pairper convertire il parametro in value_type(es .: aggiunta consta first_type)
  • std::map<int, int>::value_type non lascia assolutamente alcun dubbio in quanto è direttamente il tipo di parametro previsto da insert funzione membro.

Alla fine, eviterei di utilizzare operator[]quando l'obiettivo è l'inserimento, a meno che non ci siano costi aggiuntivi nella costruzione e nell'assegnazione del default mapped_type, e che non mi interessa determinare se una nuova chiave è stata effettivamente inserita. Quando si utilizza insert, la costruzione di a value_typeè probabilmente la strada da percorrere.


la conversione da Key a const Key in make_pair () richiede davvero un'altra chiamata di funzione? Sembra che un cast implicito sarebbe sufficiente quale compilatore dovrebbe essere felice di farlo.
galactica

99

A partire da C ++ 11, hai due principali opzioni aggiuntive. Innanzitutto, puoi usare insert()con la sintassi di inizializzazione dell'elenco:

function.insert({0, 42});

Questo è funzionalmente equivalente a

function.insert(std::map<int, int>::value_type(0, 42));

ma molto più conciso e leggibile. Come hanno notato altre risposte, questo ha diversi vantaggi rispetto alle altre forme:

  • L' operator[]approccio richiede che il tipo mappato sia assegnabile, il che non è sempre il caso.
  • L' operator[]approccio può sovrascrivere gli elementi esistenti e non ti dà modo di sapere se ciò è accaduto.
  • Le altre forme di insertquello che elenchi implicano una conversione di tipo implicita, che potrebbe rallentare il codice.

Lo svantaggio principale è che questo modulo richiedeva che la chiave e il valore fossero copiabili, quindi non funzionava, ad esempio, con una mappa con unique_ptrvalori. Questo problema è stato risolto nello standard, ma potrebbe non aver ancora raggiunto l'implementazione della libreria standard.

In secondo luogo, puoi utilizzare il emplace()metodo:

function.emplace(0, 42);

Questo è più conciso di qualsiasi forma di insert(), funziona bene con i tipi di solo movimento come unique_ptr, e teoricamente potrebbe essere leggermente più efficiente (sebbene un compilatore decente dovrebbe ottimizzare la differenza). L'unico inconveniente principale è che potrebbe sorprendere un po 'i tuoi lettori, poiché i emplacemetodi di solito non vengono utilizzati in questo modo.


8
c'è anche il nuovo insert_or_assign e try_emplace
sp2danny

12

La prima versione:

function[0] = 42; // version 1

può o non può inserire il valore 42 nella mappa. Se la chiave 0esiste, assegnerà 42 a quella chiave, sovrascrivendo il valore di quella chiave. Altrimenti inserisce la coppia chiave / valore.

Le funzioni di inserimento:

function.insert(std::map<int, int>::value_type(0, 42));  // version 2
function.insert(std::pair<int, int>(0, 42));             // version 3
function.insert(std::make_pair(0, 42));                  // version 4

d'altra parte, non fare nulla se la chiave 0esiste già nella mappa. Se la chiave non esiste, inserisce la coppia chiave / valore.

Le tre funzioni di inserimento sono quasi identiche. std::map<int, int>::value_typeè il typedefper std::pair<const int, int>, e std::make_pair()ovviamente produce unstd::pair<> magia di deduzione tramite template. Il risultato finale, tuttavia, dovrebbe essere lo stesso per le versioni 2, 3 e 4.

Quale dovrei usare? Personalmente preferisco la versione 1; è conciso e "naturale". Ovviamente, se il suo comportamento di sovrascrittura non è desiderato, allora preferirei la versione 4, poiché richiede meno battitura rispetto alle versioni 2 e 3. Non so se esiste un unico modo di fatto per inserire coppie chiave / valore in un std::map.

Un altro modo per inserire valori in una mappa tramite uno dei suoi costruttori:

std::map<int, int> quadratic_func;

quadratic_func[0] = 0;
quadratic_func[1] = 1;
quadratic_func[2] = 4;
quadratic_func[3] = 9;

std::map<int, int> my_func(quadratic_func.begin(), quadratic_func.end());

5

Se vuoi sovrascrivere l'elemento con il tasto 0

function[0] = 42;

Altrimenti:

function.insert(std::make_pair(0, 42));

5

Poiché C ++ 17 std::map offre due nuovi metodi di inserimento: insert_or_assign()e try_emplace(), come menzionato anche nel commento di sp2danny .

insert_or_assign()

Fondamentalmente, insert_or_assign()è una versione "migliorata" di operator[]. Al contrario operator[], insert_or_assign()non richiede che il tipo di valore della mappa sia costruibile di default. Ad esempio, il codice seguente non viene compilato, perché MyClassnon ha un costruttore predefinito:

class MyClass {
public:
    MyClass(int i) : m_i(i) {};
    int m_i;
};

int main() {
    std::map<int, MyClass> myMap;

    // VS2017: "C2512: 'MyClass::MyClass' : no appropriate default constructor available"
    // Coliru: "error: no matching function for call to 'MyClass::MyClass()"
    myMap[0] = MyClass(1);

    return 0;
}

Tuttavia, se si sostituisce myMap[0] = MyClass(1);con la riga seguente, il codice viene compilato e l'inserimento avviene come previsto:

myMap.insert_or_assign(0, MyClass(1));

Inoltre, in modo simile a insert(), insert_or_assign()restituisce a pair<iterator, bool>. Il valore booleano è truese si è verificato un inserimento e falsese è stata eseguita un'assegnazione. L'iteratore punta all'elemento che è stato inserito o aggiornato.

try_emplace()

Simile a quanto sopra, try_emplace()è un "miglioramento" di emplace(). Al contrario emplace(), try_emplace()non modifica i suoi argomenti se l'inserimento fallisce a causa di una chiave già esistente nella mappa. Ad esempio, il codice seguente tenta di posizionare un elemento con una chiave già memorizzata nella mappa (vedere *):

int main() {
    std::map<int, std::unique_ptr<MyClass>> myMap2;
    myMap2.emplace(0, std::make_unique<MyClass>(1));

    auto pMyObj = std::make_unique<MyClass>(2);    
    auto [it, b] = myMap2.emplace(0, std::move(pMyObj));  // *

    if (!b)
        std::cout << "pMyObj was not inserted" << std::endl;

    if (pMyObj == nullptr)
        std::cout << "pMyObj was modified anyway" << std::endl;
    else
        std::cout << "pMyObj.m_i = " << pMyObj->m_i <<  std::endl;

    return 0;
}

Output (almeno per VS2017 e Coliru):

pMyObj non è stato inserito
pMyObj è stato comunque modificato

Come puoi vedere, pMyObjnon punta più all'oggetto originale. Tuttavia, se si sostituisce auto [it, b] = myMap2.emplace(0, std::move(pMyObj));con il codice seguente, l'output ha un aspetto diverso, perché pMyObjrimane invariato:

auto [it, b] = myMap2.try_emplace(0, std::move(pMyObj));

Produzione:

pMyObj non è stato inserito
pMyObj pMyObj.m_i = 2

Codice su Coliru

Nota: ho cercato di mantenere le mie spiegazioni il più brevi e semplici possibile per inserirle in questa risposta. Per una descrizione più precisa e completa, consiglio di leggere questo articolo su Fluent C ++ .


3

Ho eseguito alcuni confronti temporali tra le versioni di cui sopra:

function[0] = 42;
function.insert(std::map<int, int>::value_type(0, 42));
function.insert(std::pair<int, int>(0, 42));
function.insert(std::make_pair(0, 42));

Risulta che le differenze di tempo tra le versioni dell'inserto sono minime.

#include <map>
#include <vector>
#include <boost/date_time/posix_time/posix_time.hpp>
using namespace boost::posix_time;
class Widget {
public:
    Widget() {
        m_vec.resize(100);
        for(unsigned long it = 0; it < 100;it++) {
            m_vec[it] = 1.0;
        }
    }
    Widget(double el)   {
        m_vec.resize(100);
        for(unsigned long it = 0; it < 100;it++) {
            m_vec[it] = el;
        }
    }
private:
    std::vector<double> m_vec;
};


int main(int argc, char* argv[]) {



    std::map<int,Widget> map_W;
    ptime t1 = boost::posix_time::microsec_clock::local_time();    
    for(int it = 0; it < 10000;it++) {
        map_W.insert(std::pair<int,Widget>(it,Widget(2.0)));
    }
    ptime t2 = boost::posix_time::microsec_clock::local_time();
    time_duration diff = t2 - t1;
    std::cout << diff.total_milliseconds() << std::endl;

    std::map<int,Widget> map_W_2;
    ptime t1_2 = boost::posix_time::microsec_clock::local_time();    
    for(int it = 0; it < 10000;it++) {
        map_W_2.insert(std::make_pair(it,Widget(2.0)));
    }
    ptime t2_2 = boost::posix_time::microsec_clock::local_time();
    time_duration diff_2 = t2_2 - t1_2;
    std::cout << diff_2.total_milliseconds() << std::endl;

    std::map<int,Widget> map_W_3;
    ptime t1_3 = boost::posix_time::microsec_clock::local_time();    
    for(int it = 0; it < 10000;it++) {
        map_W_3[it] = Widget(2.0);
    }
    ptime t2_3 = boost::posix_time::microsec_clock::local_time();
    time_duration diff_3 = t2_3 - t1_3;
    std::cout << diff_3.total_milliseconds() << std::endl;

    std::map<int,Widget> map_W_0;
    ptime t1_0 = boost::posix_time::microsec_clock::local_time();    
    for(int it = 0; it < 10000;it++) {
        map_W_0.insert(std::map<int,Widget>::value_type(it,Widget(2.0)));
    }
    ptime t2_0 = boost::posix_time::microsec_clock::local_time();
    time_duration diff_0 = t2_0 - t1_0;
    std::cout << diff_0.total_milliseconds() << std::endl;

    system("pause");
}

Questo dà rispettivamente per le versioni (ho eseguito il file 3 volte, da qui le 3 differenze di tempo consecutive per ciascuna):

map_W.insert(std::pair<int,Widget>(it,Widget(2.0)));

2198 ms, 2078 ms, 2072 ms

map_W_2.insert(std::make_pair(it,Widget(2.0)));

2290 ms, 2037 ms, 2046 ms

 map_W_3[it] = Widget(2.0);

2592 ms, 2278 ms, 2296 ms

 map_W_0.insert(std::map<int,Widget>::value_type(it,Widget(2.0)));

2234 ms, 2031 ms, 2027 ms

Pertanto, i risultati tra diverse versioni di inserti possono essere trascurati (non è stato tuttavia eseguito un test di ipotesi)!

La map_W_3[it] = Widget(2.0);versione richiede circa il 10-15% di tempo in più per questo esempio a causa di un'inizializzazione con il costruttore predefinito per Widget.


2

In breve, l' []operatore è più efficiente per l'aggiornamento dei valori perché implica la chiamata del costruttore predefinito del tipo di valore e quindi l'assegnazione di un nuovo valore, mentreinsert() è più efficiente per l'aggiunta di valori.

Lo snippet citato da Efficace STL: 50 modi specifici per migliorare l'utilizzo della libreria di modelli standard di Scott Meyers, elemento 24 potrebbe essere d'aiuto.

template<typename MapType, typename KeyArgType, typename ValueArgType>
typename MapType::iterator
insertKeyAndValue(MapType& m, const KeyArgType&k, const ValueArgType& v)
{
    typename MapType::iterator lb = m.lower_bound(k);

    if (lb != m.end() && !(m.key_comp()(k, lb->first))) {
        lb->second = v;
        return lb;
    } else {
        typedef typename MapType::value_type MVT;
        return m.insert(lb, MVT(k, v));
    }
}

Potresti decidere di scegliere una versione priva di programmazione generica di questo, ma il punto è che trovo questo paradigma (differenziando "aggiungi" e "aggiorna") estremamente utile.


1

Se vuoi inserire un elemento in std :: map - usa la funzione insert (), e se vuoi trovare un elemento (per chiave) e assegnargli un po '- usa l'operatore [].

Per semplificare l'inserimento, usa boost :: assign libreria, in questo modo:

using namespace boost::assign;

// For inserting one element:

insert( function )( 0, 41 );

// For inserting several elements:

insert( function )( 0, 41 )( 0, 42 )( 0, 43 );

1

Ho solo cambiato un po 'il problema (mappa delle stringhe) per mostrare un altro interesse di inserimento:

std::map<int, std::string> rancking;

rancking[0] = 42;  // << some compilers [gcc] show no error

rancking.insert(std::pair<int, std::string>(0, 42));// always a compile error

il fatto che il compilatore non mostra errori su "rancking [1] = 42;" può avere un impatto devastante!


I compilatori non mostrano un errore per il primo perché std::string::operator=(char)esiste, ma mostrano un errore per il secondo perché il costruttore std::string::string(char)non esiste. Non dovrebbe produrre un errore perché C ++ interpreta sempre liberamente qualsiasi letterale in stile intero comechar , quindi questo non è un bug del compilatore, ma è invece un errore del programmatore. Fondamentalmente, sto solo dicendo che se questo introduce o meno un bug nel tuo codice è qualcosa che devi stare attento a te stesso. A proposito, puoi stampare rancking[0]e un compilatore che usa ASCII produrrà *, che è (char)(42).
Keith M

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.