Was ist die bevorzugte / idiomatische Methode zum Einfügen in eine Karte?

111

Ich habe vier verschiedene Möglichkeiten zum Einfügen von Elementen in a identifiziert 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));

Welcher davon ist der bevorzugte / idiomatische Weg? (Und gibt es einen anderen Weg, an den ich nicht gedacht habe?)

Fredoverflow
quelle
26
Ihre Karte sollte "Antworten" heißen, nicht "Funktion"
Vincent Robert
2
@ Vincent: Hm? Eine Funktion ist im Grunde eine Karte zwischen zwei Mengen.
Fredoverflow
7
@FredOverflow: scheint, dass Vincents Kommentar ein Witz über ein bestimmtes Buch ist ...
Victor Sorokin
1
Es scheint dem Original zu widersprechen - 42 kann nicht gleichzeitig die Antwort auf (a) das Leben, das Universum und alles und (b) nichts sein. Aber wie drückt man dann das Leben, das Universum und alles als int aus?
Stuart Golodetz
19
@sgolodetz Du kannst alles mit einem int ausdrücken, das groß genug ist.
Yakov Galka

Antworten:

90

Zu allererst operator[]und insertElementfunktionen sind nicht funktional äquivalent:

  • Das operator[]wird die Suche nach dem Schlüssel, einen einfügen Standard konstruiert Wert , wenn nicht gefunden, und geben einen Hinweis auf die Sie einen Wert zuweisen. Offensichtlich kann dies ineffizient sein, wenn das Unternehmen mapped_typedavon profitieren kann, direkt initialisiert zu werden, anstatt standardmäßig erstellt und zugewiesen zu werden. Diese Methode macht es auch unmöglich festzustellen, ob tatsächlich eine Einfügung stattgefunden hat oder ob Sie nur den Wert für einen zuvor eingefügten Schlüssel überschrieben haben
  • Die insertElementfunktion hat keine Auswirkung, wenn der Schlüssel bereits in der Karte vorhanden ist, und gibt, obwohl er häufig vergessen wird, einen zurück, std::pair<iterator, bool>der von Interesse sein kann (insbesondere, um festzustellen, ob das Einfügen tatsächlich erfolgt ist).

Von allen aufgelisteten Anrufmöglichkeiten insertsind alle drei nahezu gleichwertig. Lassen Sie uns zur Erinnerung die insertSignatur im Standard betrachten:

typedef pair<const Key, T> value_type;

  /* ... */

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

Wie unterscheiden sich die drei Anrufe?

  • std::make_pairTemplate - Argument Abzug beruht auf und konnte (und in diesem Fall wird ) produziert etwas von einem anderen Typ als die tatsächlichen value_typevon der Karte, das einen zusätzlichen Aufruf benötigt std::pairVorlage Konstruktor , um zu konvertieren value_type(dh: das Hinzufügen constzu first_type)
  • std::pair<int, int>erfordert auch einen zusätzlichen Aufruf des Vorlagenkonstruktors von std::pair, um den Parameter in zu konvertieren value_type(dh: Hinzufügen constzu first_type)
  • std::map<int, int>::value_typelässt absolut keinen Zweifel offen, da es sich direkt um den von der Elementfunktion erwarteten Parametertyp handelt insert.

Am Ende würde ich es vermeiden, zu verwenden, operator[]wenn das Ziel das Einfügen ist, es sei denn, es entstehen keine zusätzlichen Kosten für das Standardkonstruieren und Zuweisen des mapped_typeund es ist mir egal, ob ein neuer Schlüssel effektiv eingefügt wurde. Bei der Verwendung insertist das Konstruieren von a value_typewahrscheinlich der richtige Weg.

Eiskriminalität
quelle
fordert die Konvertierung von Key zu const Key in make_pair () wirklich einen weiteren Funktionsaufruf an? Es scheint, dass eine implizite Besetzung ausreichen würde, welcher Compiler dies gerne tun würde.
Galactica
99

Ab C ++ 11 haben Sie zwei wichtige zusätzliche Optionen. Erstens können Sie insert()mit der Listeninitialisierungssyntax verwenden:

function.insert({0, 42});

Dies ist funktional äquivalent zu

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

aber viel prägnanter und lesbarer. Wie andere Antworten festgestellt haben, hat dies gegenüber den anderen Formen mehrere Vorteile:

  • Das operator[] Ansatz erfordert, dass der zugeordnete Typ zuweisbar ist, was nicht immer der Fall ist.
  • Das operator[] Ansatz kann vorhandene Elemente überschreiben und gibt Ihnen keine Möglichkeit zu erkennen, ob dies geschehen ist.
  • Die anderen Formen insert, die Sie auflisten, beinhalten eine implizite Typkonvertierung, die Ihren Code verlangsamen kann.

Der Hauptnachteil besteht darin, dass für dieses Formular der Schlüssel und der Wert kopierbar sein müssen, sodass es beispielsweise mit einer Karte mit nicht funktioniert unique_ptr Werten . Dies wurde im Standard behoben, aber der Fix hat möglicherweise Ihre Standardbibliotheksimplementierung noch nicht erreicht.

Zweitens können Sie die emplace()Methode verwenden:

function.emplace(0, 42);

Dies ist prägnanter als jede andere Form von insert(), funktioniert gut mit Nur-Verschieben-Typen wie unique_ptrund kann theoretisch etwas effizienter sein (obwohl ein anständiger Compiler den Unterschied optimieren sollte). Der einzige große Nachteil ist, dass es Ihre Leser ein wenig überraschen kann, da emplaceMethoden normalerweise nicht so verwendet werden.

Geoff Romer
quelle
8
Es gibt auch die neuen insert_or_assign und try_emplace
sp2danny
11

Die erste Version:

function[0] = 42; // version 1

kann den Wert 42 in die Karte einfügen oder nicht. Wenn der Schlüssel 0vorhanden ist, weist er diesem Schlüssel 42 zu und überschreibt den Wert, den dieser Schlüssel hatte. Andernfalls wird das Schlüssel / Wert-Paar eingefügt.

Die Einfügefunktionen:

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

Tun Sie andererseits nichts, wenn der Schlüssel 0bereits in der Karte vorhanden ist. Wenn der Schlüssel nicht vorhanden ist, wird das Schlüssel / Wert-Paar eingefügt.

Die drei Einfügefunktionen sind nahezu identisch. std::map<int, int>::value_typeist das typedeffür std::pair<const int, int>und std::make_pair()produziert offensichtlich einstd::pair<> Via-Template-Abzugsmagie. Das Endergebnis sollte jedoch für die Versionen 2, 3 und 4 gleich sein.

Welches würde ich verwenden? Ich persönlich bevorzuge Version 1; es ist prägnant und "natürlich". Wenn das Überschreibverhalten nicht erwünscht ist, würde ich natürlich Version 4 bevorzugen, da weniger Eingaben erforderlich sind als in den Versionen 2 und 3. Ich weiß nicht, ob es de facto eine einzige Möglichkeit gibt, Schlüssel / Wert-Paare in a einzufügen std::map.

Eine andere Möglichkeit, Werte über einen ihrer Konstruktoren in eine Karte einzufügen:

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());
In silico
quelle
5

Wenn Sie das Element mit der Taste 0 überschreiben möchten

function[0] = 42;

Andernfalls:

function.insert(std::make_pair(0, 42));
Viktor Sehr
quelle
5

Da C ++ 17 std::map zwei neue Einfügemethoden bietet: insert_or_assign()und try_emplace(), wie auch im Kommentar von sp2danny erwähnt .

insert_or_assign()

Grundsätzlich insert_or_assign()ist eine "verbesserte" Version von operator[]. Im Gegensatz zu operator[], insert_or_assign()erfordert nicht den Wert Typ der Karte standardmäßig konstruierbar zu sein. Der folgende Code wird beispielsweise nicht kompiliert, da er MyClasskeinen Standardkonstruktor hat:

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

Wenn Sie jedoch myMap[0] = MyClass(1);durch die folgende Zeile ersetzen , wird der Code kompiliert und das Einfügen erfolgt wie vorgesehen:

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

Außerdem ähnelt insert(), insert_or_assign()kehrt ein pair<iterator, bool>. Der Boolesche Wert gibt an, trueob eine Einfügung erfolgt ist und falseob eine Zuweisung vorgenommen wurde. Der Iterator zeigt auf das Element, das eingefügt oder aktualisiert wurde.

try_emplace()

Ähnlich wie oben try_emplace()ist eine "Verbesserung" von emplace(). Im Gegensatz dazu emplace()werden try_emplace()die Argumente nicht geändert, wenn das Einfügen aufgrund eines bereits in der Karte vorhandenen Schlüssels fehlschlägt. Der folgende Code versucht beispielsweise, ein Element mit einem Schlüssel zu ersetzen, der bereits in der Karte gespeichert ist (siehe *):

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

Ausgabe (zumindest für VS2017 und Coliru):

pMyObj wurde nicht eingefügt
pMyObj wurde trotzdem geändert

Wie Sie sehen, zeigt pMyObjnicht mehr auf das ursprüngliche Objekt. Wenn Sie jedoch auto [it, b] = myMap2.emplace(0, std::move(pMyObj));durch den folgenden Code ersetzen , sieht die Ausgabe anders aus, da sie pMyObjunverändert bleibt:

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

Ausgabe:

pMyObj wurde nicht eingefügt
pMyObj pMyObj.m_i = 2

Code auf Coliru

Bitte beachten Sie: Ich habe versucht, meine Erklärungen so kurz und einfach wie möglich zu halten, um sie in diese Antwort zu integrieren. Für eine genauere und umfassendere Beschreibung empfehle ich, diesen Artikel über Fluent C ++ zu lesen .

hupen
quelle
3

Ich habe einige Zeitvergleiche zwischen den oben genannten Versionen durchgeführt:

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

Es stellt sich heraus, dass die Zeitunterschiede zwischen den Insert-Versionen winzig sind.

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

Dies ergibt jeweils für die Versionen (ich habe die Datei 3 mal ausgeführt, daher die 3 aufeinander folgenden Zeitunterschiede für jede):

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

Daher können Ergebnisse zwischen verschiedenen Insert-Versionen vernachlässigt werden (es wurde jedoch kein Hypothesentest durchgeführt)!

Die map_W_3[it] = Widget(2.0);Version benötigt für dieses Beispiel aufgrund einer Initialisierung mit dem Standardkonstruktor für Widget etwa 10-15% mehr Zeit.

user3116431
quelle
2

Kurz gesagt, der []Operator ist effizienter für die Aktualisierung von Werten, da er den Standardkonstruktor des Werttyps aufruft und ihm dann einen neuen Wert zuweistinsert() er effizienter für das Hinzufügen von Werten ist.

Das zitierte Snippet aus Effective STL: 50 spezifische Möglichkeiten zur Verbesserung der Verwendung der Standardvorlagenbibliothek von Scott Meyers, Punkt 24, könnte hilfreich sein.

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

Sie können sich für eine Version ohne generische Programmierung entscheiden, aber der Punkt ist, dass ich dieses Paradigma (Unterscheidung zwischen "Hinzufügen" und "Aktualisieren") äußerst nützlich finde.

Galactica
quelle
1

Wenn Sie ein Element in std :: map einfügen möchten, verwenden Sie die Funktion insert (), und wenn Sie ein Element (nach Schlüssel) suchen und ihm ein Element zuweisen möchten, verwenden Sie den Operator [].

Verwenden Sie zur Vereinfachung des Einfügens die Datei boost :: assign wie folgt:

using namespace boost::assign;

// For inserting one element:

insert( function )( 0, 41 );

// For inserting several elements:

insert( function )( 0, 41 )( 0, 42 )( 0, 43 );
Denis Shevchenko
quelle
1

Ich ändere das Problem nur ein wenig (Map of Strings), um ein weiteres Interesse an Insert zu zeigen:

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

die Tatsache, dass der Compiler keinen Fehler bei "rancking [1] = 42;" kann verheerende Auswirkungen haben!

jo_
quelle
Compiler zeigen keinen Fehler für den ersteren an, weil std::string::operator=(char)vorhanden, aber sie zeigen einen Fehler für den letzteren, weil der Konstruktor std::string::string(char)nicht vorhanden ist. Es sollte keinen Fehler erzeugen, da C ++ jedes ganzzahlige Literal immer frei als interpretiert. charDies ist also kein Compiler-Fehler, sondern ein Programmiererfehler. Grundsätzlich sage ich nur, dass Sie selbst darauf achten müssen, ob dies einen Fehler in Ihrem Code verursacht oder nicht. Übrigens können Sie drucken rancking[0]und ein Compiler mit ASCII wird ausgeben *, das heißt (char)(42).
Keith M