Ich verwende zum ersten Mal Karten und habe festgestellt, dass es viele Möglichkeiten gibt, ein Element einzufügen. Sie können emplace()
, operator[]
oder insert()
, plus Varianten wie value_type
oder verwenden make_pair
. Obwohl es viele Informationen zu allen gibt und Fragen zu bestimmten Fällen, kann ich das Gesamtbild immer noch nicht verstehen. Meine beiden Fragen sind also:
Was ist der Vorteil eines jeden von ihnen gegenüber dem anderen?
Gab es eine Notwendigkeit, dem Standard Emplace hinzuzufügen? Gibt es etwas, was ohne es vorher nicht möglich war?
operator[]
basiert auftry_emplace
. Es kann auch erwähnenswert seininsert_or_assign
.Antworten:
Im speziellen Fall einer Karte waren die alten Optionen nur zwei:
operator[]
undinsert
(verschiedene Geschmacksrichtungen voninsert
). Also werde ich anfangen, diese zu erklären.Das
operator[]
ist ein Find-or-Add- Operator. Es wird versucht, ein Element mit dem angegebenen Schlüssel in der Karte zu finden. Wenn es vorhanden ist, wird ein Verweis auf den gespeicherten Wert zurückgegeben. Ist dies nicht der Fall, wird ein neues Element erstellt, das mit der Standardinitialisierung eingefügt wurde, und es wird ein Verweis darauf zurückgegeben.Die
insert
Funktion (in der Einzelelement-Variante) nimmt einvalue_type
(std::pair<const Key,Value>
), verwendet den Schlüssel ( Elementfirst
) und versucht, ihn einzufügen. Dastd::map
Duplikate nicht zulässig sind, wenn ein Element vorhanden ist, wird nichts eingefügt.Der erste Unterschied zwischen beiden besteht darin, dass
operator[]
ein standardmäßig initialisierter Wert erstellt werden muss. Daher ist er für Werttypen, die nicht standardmäßig initialisiert werden können, unbrauchbar. Der zweite Unterschied zwischen den beiden besteht darin, was passiert, wenn bereits ein Element mit dem angegebenen Schlüssel vorhanden ist. Dieinsert
Funktion ändert nicht den Status der Karte, sondern gibt stattdessen einen Iterator an das Element zurück (und einenfalse
Hinweis darauf, dass es nicht eingefügt wurde).Im Falle des
insert
Arguments handelt es sich um ein Objekt vonvalue_type
, das auf verschiedene Arten erstellt werden kann. Sie können es direkt mit dem entsprechenden Typvalue_type
erstellen oder jedes Objekt übergeben, aus dem das erstellt werden kann. Hierstd::make_pair
kommt es ins Spiel, da es die einfache Erstellung vonstd::pair
Objekten ermöglicht, obwohl es wahrscheinlich nicht das ist, was Sie wollen ...Der Nettoeffekt der folgenden Aufrufe ist ähnlich :
Aber die sind nicht wirklich gleich ... [1] und [2] sind tatsächlich gleichwertig. In beiden Fällen erstellt der Code ein temporäres Objekt desselben Typs (
std::pair<const K,V>
) und übergibt es an dieinsert
Funktion. Dieinsert
Funktion erstellt den entsprechenden Knoten im binären Suchbaum und kopiert dann denvalue_type
Teil aus dem Argument auf den Knoten. Der Vorteil der Verwendungvalue_type
ist, dass Sievalue_type
immer übereinstimmenvalue_type
und den Typ derstd::pair
Argumente nicht falsch eingeben können !Der Unterschied liegt in [3]. Die Funktion
std::make_pair
ist eine Vorlagenfunktion, die eine erstelltstd::pair
. Die Unterschrift lautet:Ich habe die Vorlagenargumente absichtlich nicht angegeben
std::make_pair
, da dies die übliche Verwendung ist. Und die Implikation ist, dass die Vorlagenargumente aus dem Aufruf abgeleitet werden, in diesem Fall zu seinT==K,U==V
, so dass der Aufruf von astd::make_pair
zurückgibtstd::pair<K,V>
(beachten Sie das Fehlenconst
). Die Signatur erfordert,value_type
dass sie nahe ist, aber nicht mit dem vom Aufruf an zurückgegebenen Wert übereinstimmtstd::make_pair
. Da es nah genug ist, wird eine temporäre Datei des richtigen Typs erstellt und durch Kopieren initialisiert. Dies wird wiederum auf den Knoten kopiert, wodurch insgesamt zwei Kopien erstellt werden.Dies kann durch Angabe der Vorlagenargumente behoben werden:
Dies ist jedoch immer noch fehleranfällig, genauso wie die explizite Eingabe des Typs in case [1].
Bis zu diesem Punkt haben wir verschiedene Arten des Aufrufs
insert
, die die Erstellung desvalue_type
externen Objekts und die Kopie dieses Objekts in den Container erfordern . Alternativ können Sie verwenden,operator[]
wenn der Typ standardmäßig konstruierbar und zuweisbar ist (absichtlich nur in fokussiertm[k]=v
) und die Standardinitialisierung eines Objekts und die Kopie des Werts in dieses Objekt erforderlich sind .In C ++ 11 gibt es mit variablen Vorlagen und perfekter Weiterleitung eine neue Möglichkeit, Elemente durch Einfügen (Erstellen an Ort und Stelle) in einen Container einzufügen . Die
emplace
Funktionen in den verschiedenen Containern machen im Grunde dasselbe: Anstatt eine Quelle zum Kopieren in den Container zu erhalten, übernimmt die Funktion die Parameter, die an den Konstruktor des im Container gespeicherten Objekts weitergeleitet werden.In [5] wird das
std::pair<const K, V>
nicht erstellt und übergebenemplace
, sondern es werden Verweise auf das Objektt
undu
übergeben,emplace
die sie an den Konstruktor desvalue_type
Unterobjekts innerhalb der Datenstruktur weiterleiten . In diesem Fall werden überhaupt keine Kopien von erstelltstd::pair<const K,V>
, wasemplace
gegenüber den C ++ 03-Alternativen von Vorteil ist . Wie im Fallinsert
wird der Wert in der Karte nicht überschrieben.Eine interessante Frage, über die ich nicht nachgedacht hatte, ist, wie
emplace
sie tatsächlich für eine Karte implementiert werden kann, und das ist im allgemeinen Fall kein einfaches Problem.quelle
mapped_type
Paarkopie speichern, was gut ist, weil keine Paarkopie keine Isntance-Kopie bedeutet. Was wir wollen, ist die Konstruktion dermapped_type
im Paar und die Paarkonstruktion in der Karte. Daher fehlen sowohl diestd::pair::emplace
Funktion als auch die Weiterleitungsunterstützung inmap::emplace
. In der aktuellen Form müssen Sie dem Paarkonstruktor noch einen konstruierten mapped_type geben, der ihn einmal kopiert. Es ist besser als zweimal, aber immer noch nicht gut.insert_or_assign
undtry_emplace
(beide aus C ++ 17) zu aktualisieren , die dazu beitragen, einige Lücken in der Funktionalität der vorhandenen Methoden zu schließen.Einfügen: Nutzt die rWert-Referenz, um die tatsächlich erstellten Objekte zu verwenden. Dies bedeutet, dass kein Konstruktor zum Kopieren oder Verschieben aufgerufen wird, was für GROSSE Objekte gut ist! O (log (N)) Zeit.
Einfügen: Enthält Überladungen für Standard-L-Wert-Referenz und R-Wert-Referenz sowie Iteratoren für Listen der einzufügenden Elemente und "Hinweise" auf die Position, zu der ein Element gehört. Die Verwendung eines "Hinweis" -Iterators kann dazu führen, dass die Zeit, die für das Einfügen benötigt wird, auf die Contant-Zeit reduziert wird, andernfalls ist es die Zeit O (log (N)).
Operator []: Überprüft, ob das Objekt vorhanden ist, und ändert in diesem Fall den Verweis auf dieses Objekt, verwendet andernfalls den angegebenen Schlüssel und Wert, um make_pair für die beiden Objekte aufzurufen, und führt dann die gleiche Arbeit wie die Einfügefunktion aus. Dies ist die Zeit O (log (N)).
make_pair: Macht kaum mehr als ein Paar.
Es bestand keine "Notwendigkeit", dem Standard Emplace hinzuzufügen. In c ++ 11 wurde meiner Meinung nach der Referenztyp && hinzugefügt. Dies beseitigte die Notwendigkeit einer Verschiebungssemantik und ermöglichte die Optimierung einer bestimmten Art der Speicherverwaltung. Insbesondere die rWertreferenz. Der überladene Einfügeoperator (value_type &&) nutzt die In_place-Semantik nicht aus und ist daher viel weniger effizient. Es bietet zwar die Möglichkeit, mit Wertreferenzen umzugehen, ignoriert jedoch deren Hauptzweck, nämlich die Konstruktion von Objekten.
quelle
emplace()
Dies ist einfach die einzige Möglichkeit, ein Element einzufügen, das nicht kopiert oder verschoben werden kann. (& ja, vielleicht, um am effizientesten einen einzufügen, dessen Kopier- und Verschiebungskonstruktoren viel mehr kosten als die Konstruktion, wenn so etwas existiert.) Es scheint auch, dass Sie die Idee falsch verstanden haben: Es geht nicht darum, die r-Wert-Referenz auszunutzen um die tatsächlichen Objekte zu verwenden, die Sie bereits erstellt haben "; Es wurde noch kein Objekt erstellt und Sie leitenmap
die Argumente weiter , die zum Erstellen in sich selbst erforderlich sind. Du machst das Objekt nicht.Neben den Optimierungsmöglichkeiten und der einfacheren Syntax besteht ein wichtiger Unterschied zwischen Einfügen und Einfügen darin, dass letzteres explizite Konvertierungen ermöglicht. (Dies gilt für die gesamte Standardbibliothek, nicht nur für Karten.)
Hier ist ein Beispiel, um zu demonstrieren:
Dies ist zwar ein sehr spezifisches Detail, aber wenn Sie mit Ketten benutzerdefinierter Conversions arbeiten, sollten Sie dies berücksichtigen.
quelle
v.emplace(v.end(), 10, 10);
... oder müssten Sie jetzt verwenden :v.emplace(v.end(), foo(10, 10) );
?emplace
, verwenden eine Klasse, die einen einzelnen Parameter akzeptiert. IMO würde es tatsächlich die Natur der variadischen Syntax von emplace viel klarer machen, wenn in Beispielen mehrere Parameter verwendet würden.Der folgende Code kann Ihnen helfen, die "Gesamtidee" zu verstehen, wie sich diese
insert()
unterscheidetemplace()
:Die Ausgabe, die ich bekam, war:
Beachte das:
Ein
unordered_map
speichertFoo
Objekte immer intern (und nicht etwaFoo *
s) als Schlüssel, die alle zerstört werden, wenn dasunordered_map
zerstört wird. Hier waren dieunordered_map
internen Schlüssel des Foos 13, 11, 5, 10, 7 und 9.unordered_map
speichert unserstd::pair<const Foo, int>
Objekt tatsächlich Objekte, die wiederum dieFoo
Objekte speichern . Um jedoch die "Gesamtidee" zu verstehen, wieemplace()
unterschiedlich sie istinsert()
(siehe hervorgehobenes Kästchen unten), ist es in Ordnung, sich dieses Objekt vorübergehendstd::pair
als vollständig passiv vorzustellen . Sobald Sie diese verstehen „big picture Idee“ , ist es wichtig, dann zu sichern und zu verstehen , wie die Verwendung dieses Vermittlerstd::pair
Objekt durchunordered_map
einleiten subtil, aber wichtig, technische.Einfügen von
foo0
,foo1
undfoo2
erforderte 2 Aufrufe an einen derFoo
Kopier- / Verschiebungskonstruktoren und 2 Aufrufe anFoo
den Destruktor (wie ich jetzt beschreibe):ein. Einsetzen jedes von
foo0
undfoo1
eine temporäre Objekt erzeugt (foo4
undfoo6
sind) , deren destructor sofort wurde dann aufgerufen , nachdem das Einsetzen abgeschlossen. Darüber hinaus wurden die Destruktoren der internenFoo
s der unordered_map (Foo
s 5 und 7) aufgerufen, als die unordered_map zerstört wurde.b. Zum Einfügen
foo2
haben wir stattdessen zunächst explizit ein nicht temporäres Paarobjekt (aufgerufenpair
) erstellt, dasFoo
den Kopierkonstruktor vonfoo2
( aufgerufenfoo8
als internes Mitglied vonpair
) aufgerufen hat . Wir haben danninsert()
dieses Paar bearbeitet, was dazu führte,unordered_map
dass der Kopierkonstruktor erneut (onfoo8
) aufgerufen wurde, um eine eigene interne Kopie (foo9
) zu erstellen . Wie beifoo
s 0 und 1 war das Endergebnis zwei Destruktoraufrufe für diese Einfügung, mit dem einzigen Unterschied, dassfoo8
der Destruktor nur aufgerufen wurde, wenn wir das Ende von erreicht hatten,main()
anstatt unmittelbar nachinsert()
Beendigung aufgerufen zu werden .Das Einfügen
foo3
führte zu nur einem Aufruf des Kopier- / Verschiebungskonstruktors (foo10
intern imunordered_map
) erstellt und nur zu einem Aufruf desFoo
Destruktors. (Ich werde später darauf zurückkommen).Für
foo11
kamen wir direkt die ganze Zahl 11 ,emplace(11, d)
so dassunordered_map
der Anruf würdeFoo(int)
Konstruktor während der Ausführung innerhalb seiner istemplace()
Methode. Anders als in (2) und (3) brauchten wir dazu nicht einmal ein vorbestehendesfoo
Objekt. Beachten Sie, dass nur 1 Aufruf einesFoo
Konstruktors aufgetreten ist (der erstellt wurdefoo11
).Wir haben dann die ganze Zahl 12 direkt an übergeben
insert({12, d})
. Anders als beiemplace(11, d)
(dieser Rückruf führte nur zu einem Aufruf einesFoo
Konstruktors) führte dieser Aufruf zuinsert({12, d})
zwei Aufrufen desFoo
Konstruktors (Erstellenfoo12
undfoo13
).Dies zeigt, was der Hauptunterschied zwischen
insert()
undemplace()
ist:Hinweis: Der Grund für das " fast " in " fast immer " oben wird in I) unten erläutert.
umap.emplace(foo3, d)
genanntFoo
‚s nicht konstanten Copykonstruktor ist folgende: Da wir verwendenemplace()
, der Compiler, der weißfoo3
(eine nicht-constFoo
gemeint Objekt) ein Argument zu einem gewissen seinFoo
Konstruktor. In diesem Fall ist der am besten geeigneteFoo
Konstruktor der nicht konstante KopierkonstruktorFoo(Foo& f2)
. Aus diesem Grund wurdeumap.emplace(foo3, d)
ein Kopierkonstruktor aufgerufen, obwohlumap.emplace(11, d)
dies nicht der Fall war.Epilog:
I. Beachten Sie, dass eine Überlastung von
insert()
tatsächlich entsprichtemplace()
. Wie auf dieser Seite cppreference.com beschrieben , entspricht die Überladungtemplate<class P> std::pair<iterator, bool> insert(P&& value)
( dh die Überladung (2)insert()
auf dieser Seite cppreference.com)emplace(std::forward<P>(value))
.II. Wohin von hier aus?
ein. Spielen Sie mit dem obigen Quellcode und der Studiendokumentation für
insert()
(z. B. hier ) undemplace()
(z. B. hier ), die online verfügbar sind. Wenn Sie eine IDE wie Eclipse oder NetBeans verwenden, können Sie sich von Ihrer IDE leicht mitteilen lassen, welche Überlastung voninsert()
oderemplace()
aufgerufen wird (halten Sie in Eclipse den Mauszeiger für eine Sekunde ruhig über dem Funktionsaufruf). Hier ist noch ein Code zum Ausprobieren:Sie werden bald feststellen, dass die Überlastung des
std::pair
Konstruktors (siehe Referenz ), die letztendlich verwendet wirdunordered_map
, einen wichtigen Einfluss darauf haben kann, wie viele Objekte kopiert, verschoben, erstellt und / oder zerstört werden und wann dies alles auftritt.b. Sehen Sie, was passiert, wenn Sie eine andere Containerklasse (z. B.
std::set
oderstd::unordered_multiset
) anstelle von verwendenstd::unordered_map
.c. Verwenden Sie nun ein
Goo
Objekt (nur eine umbenannte Kopie vonFoo
) anstelle einesint
als Bereichstyp in einemunordered_map
(dh verwenden Sieunordered_map<Foo, Goo>
anstelle vonunordered_map<Foo, int>
) und sehen Sie, wie viele und welcheGoo
Konstruktoren aufgerufen werden. (Spoiler: Es gibt einen Effekt, aber er ist nicht sehr dramatisch.)quelle
In Bezug auf Funktionalität oder Ausgabe sind beide gleich.
Für beide großen Speicher ist object emplace speicheroptimiert, wobei keine Kopierkonstruktoren verwendet werden
Für eine einfache detaillierte Erklärung https://medium.com/@sandywits/all-about-emplace-in-c-71fd15e06e44
quelle