Warum sollte ich jemals push_back anstelle von emplace_back verwenden?

231

C ++ 11-Vektoren haben die neue Funktion emplace_back. Im Gegensatz push_backzu Compiler-Optimierungen, um Kopien zu vermeiden, wird emplace_backdie perfekte Weiterleitung verwendet, um die Argumente direkt an den Konstruktor zu senden und ein Objekt an Ort und Stelle zu erstellen. Es scheint mir, dass emplace_backalles alles push_backkann, aber manchmal wird es besser (aber nie schlechter).

Welchen Grund muss ich verwenden push_back?

David Stone
quelle

Antworten:

161

Ich habe in den letzten vier Jahren viel über diese Frage nachgedacht. Ich bin zu dem Schluss gekommen, dass die meisten Erklärungen über push_backvs. emplace_backdas ganze Bild verfehlen.

Letztes Jahr hielt ich bei C ++ Now eine Präsentation zum Typabzug in C ++ 14 . Ich spreche um 13:49 Uhr über push_backvs. emplace_back, aber es gibt nützliche Informationen, die vorher einige unterstützende Beweise liefern.

Der eigentliche Hauptunterschied hat mit impliziten und expliziten Konstruktoren zu tun. Stellen Sie sich den Fall vor, in dem wir ein einzelnes Argument haben, das wir an push_backoder übergeben möchten emplace_back.

std::vector<T> v;
v.push_back(x);
v.emplace_back(x);

Nachdem Ihr optimierender Compiler dies in die Hände bekommen hat, gibt es keinen Unterschied zwischen diesen beiden Anweisungen in Bezug auf den generierten Code. Die traditionelle Weisheit ist, dass push_backein temporäres Objekt erstellt wird, in das dann verschoben wird, vwährend emplace_backdas Argument weitergeleitet und ohne Kopien oder Verschiebungen direkt an Ort und Stelle erstellt wird. Dies mag auf der Grundlage des in Standardbibliotheken geschriebenen Codes zutreffen, es wird jedoch fälschlicherweise davon ausgegangen, dass der optimierende Compiler den von Ihnen geschriebenen Code generieren muss. Die Aufgabe des optimierenden Compilers besteht darin, den Code zu generieren, den Sie geschrieben hätten, wenn Sie ein Experte für plattformspezifische Optimierungen wären und sich nicht um Wartbarkeit, sondern nur um Leistung gekümmert hätten.

Der eigentliche Unterschied zwischen diesen beiden Aussagen besteht darin, dass der Mächtigere emplace_backjeden Konstruktortyp aufruft, während der Vorsichtigere push_backnur implizite Konstruktoren aufruft. Implizite Konstruktoren sollen sicher sein. Wenn Sie implizit ein Uaus einem konstruieren können, Tsagen Sie, dass Ualle Informationen Tohne Verlust gespeichert werden können. Es ist in so ziemlich jeder Situation sicher, eine zu bestehen, Tund niemand wird etwas dagegen haben, wenn Sie es Ustattdessen zu einer machen. Ein gutes Beispiel für einen impliziten Konstruktor ist die Konvertierung von std::uint32_tnach std::uint64_t. Ein schlechtes Beispiel für eine implizite Konvertierung ist doubleto std::uint8_t.

Wir wollen bei unserer Programmierung vorsichtig sein. Wir möchten keine leistungsstarken Funktionen verwenden, da es umso einfacher ist, versehentlich etwas Falsches oder Unerwartetes zu tun, je leistungsfähiger die Funktion ist. Wenn Sie explizite Konstruktoren aufrufen möchten, benötigen Sie die Leistung von emplace_back. Wenn Sie nur implizite Konstruktoren aufrufen möchten, bleiben Sie bei der Sicherheit von push_back.

Ein Beispiel

std::vector<std::unique_ptr<T>> v;
T a;
v.emplace_back(std::addressof(a)); // compiles
v.push_back(std::addressof(a)); // fails to compile

std::unique_ptr<T>hat einen expliziten Konstruktor von T *. Da emplace_backexplizite Konstruktoren aufgerufen werden können, wird die Übergabe eines nicht besitzenden Zeigers problemlos kompiliert. Wenn vder Gültigkeitsbereich jedoch überschritten wird, versucht der Destruktor delete, diesen Zeiger aufzurufen , der nicht von zugewiesen wurde, newda es sich nur um ein Stapelobjekt handelt. Dies führt zu undefiniertem Verhalten.

Dies ist nicht nur erfundener Code. Dies war ein echter Produktionsfehler, auf den ich gestoßen bin. Der Code war std::vector<T *>, aber er besaß den Inhalt. Im Rahmen der Migration zu C ++ 11 habe ich korrekt geändert T *, std::unique_ptr<T>um anzuzeigen, dass der Vektor seinen Speicher besitzt. Allerdings war ich im Jahr 2012 diese Änderungen aus meinem Verständnis gründen, in denen ich dachte „emplace_back nicht alles push_back kann tun und mehr, also warum soll ich jemals push_back benutzen?“, So dass ich auch das geändert push_backzu emplace_back.

Hätte ich stattdessen den Code als sicherer verwendet push_back, hätte ich diesen langjährigen Fehler sofort erkannt und er wäre als Erfolg beim Upgrade auf C ++ 11 angesehen worden. Stattdessen habe ich den Fehler maskiert und erst Monate später gefunden.

David Stone
quelle
Es wäre hilfreich, wenn Sie näher erläutern könnten, was genau emplace in Ihrem Beispiel bewirkt und warum es falsch ist.
Eddi
4
@eddi: Ich habe einen Abschnitt hinzugefügt, der dies erklärt: std::unique_ptr<T>hat einen expliziten Konstruktor von T *. Da emplace_backexplizite Konstruktoren aufgerufen werden können, wird die Übergabe eines nicht besitzenden Zeigers problemlos kompiliert. Wenn vder Gültigkeitsbereich jedoch überschritten wird, versucht der Destruktor delete, diesen Zeiger aufzurufen , der nicht von zugewiesen wurde, newda es sich nur um ein Stapelobjekt handelt. Dies führt zu undefiniertem Verhalten.
David Stone
Vielen Dank für die Veröffentlichung. Ich wusste es nicht, als ich meine Antwort schrieb, aber jetzt wünschte ich, ich hätte es selbst geschrieben, als ich es später gelernt habe :) Ich möchte wirklich Leute schlagen, die zu neuen Funktionen wechseln, nur um das Hippeste zu tun, was sie finden können . Leute, die Leute haben C ++ auch vor C ++ 11 verwendet, und nicht alles daran war problematisch. Wenn Sie nicht wissen, warum Sie eine Funktion verwenden, verwenden Sie sie nicht . Ich bin so froh, dass du das gepostet hast und ich hoffe, dass es viel mehr positive Stimmen gibt, damit es über meine hinausgeht. +1
user541686
1
@CaptainJacksparrow: Es sieht so aus, als würde ich implizit und explizit sagen, wo ich sie meine. Welchen Teil hast du verwirrt?
David Stone
4
@CaptainJacksparrow: Ein explicitKonstruktor ist ein Konstruktor, auf den das Schlüsselwort explicitangewendet wird. Ein "impliziter" Konstruktor ist ein Konstruktor, der dieses Schlüsselwort nicht hat. Im Fall von std::unique_ptr's Konstruktor von T *hat der Implementierer von std::unique_ptrdiesen Konstruktor geschrieben, aber das Problem hier ist, dass der Benutzer dieses Typs aufgerufen hat emplace_back, der diesen expliziten Konstruktor aufgerufen hat. Wenn dies der Fall gewesen wäre push_back, anstatt diesen Konstruktor aufzurufen, hätte er sich auf eine implizite Konvertierung verlassen, die nur implizite Konstruktoren aufrufen kann.
David Stone
116

push_backerlaubt immer die Verwendung einer einheitlichen Initialisierung, die ich sehr mag. Zum Beispiel:

struct aggregate {
    int foo;
    int bar;
};

std::vector<aggregate> v;
v.push_back({ 42, 121 });

Auf der anderen Seite v.emplace_back({ 42, 121 });wird nicht funktionieren.

Luc Danton
quelle
58
Beachten Sie, dass dies nur für die Aggregatinitialisierung und die Initialisierung der Initialisierungsliste gilt. Wenn Sie {}einen tatsächlichen Konstruktor mithilfe der Syntax aufrufen möchten, können Sie die {}'s einfach entfernen und verwenden emplace_back.
Nicol Bolas
Dumme Fragestunde: Kann emplace_back also überhaupt nicht für Vektoren von Strukturen verwendet werden? Oder einfach nicht für diesen Stil mit dem Literal {42,121}?
Phil H
1
@ LucDanton: Wie gesagt, es gilt nur für die Initialisierung von Aggregaten und Initialisiererlisten. Sie können die Syntax verwenden, um tatsächliche Konstruktoren aufzurufen. Sie könnten einen Konstruktor angeben, der 2 Ganzzahlen akzeptiert, und dieser Konstruktor wird bei Verwendung der Syntax aufgerufen . Der Punkt ist, dass, wenn Sie versuchen , einen Konstruktor aufzurufen, dies vorzuziehen ist, da der Konstruktor direkt aufgerufen wird . Daher muss der Typ nicht kopierbar sein. {}aggregate{}emplace_back
Nicol Bolas
11
Dies wurde als Standardfehler angesehen und behoben. Siehe cplusplus.github.io/LWG/lwg-active.html#2089
David Stone
3
@DavidStone Wäre es behoben worden, wäre es nicht mehr in der "aktiven" Liste ... nein? Es scheint ein offenes Thema zu bleiben. Das neueste Update mit der Überschrift " [Verarbeitung der Batavia-Probleme am 23.08.2018] " besagt, dass " P0960 (derzeit im Flug) dieses Problem beheben sollte ". Und ich kann immer noch keinen Code kompilieren, der versucht, emplaceAggregate zu erstellen, ohne explizit einen Boilerplate-Konstruktor zu schreiben . Es ist zu diesem Zeitpunkt auch unklar, ob es als Defekt behandelt wird und somit für ein Backporting in Frage kommt oder ob Benutzer von C ++ <20 SoL bleiben.
underscore_d
80

Abwärtskompatibilität mit Pre-C ++ 11-Compilern.

user541686
quelle
21
Das scheint der Fluch von C ++ zu sein. Mit jeder neuen Version erhalten wir unzählige coole Funktionen, aber viele Unternehmen verwenden aus Gründen der Kompatibilität entweder eine alte Version oder raten davon ab, bestimmte Funktionen zu verwenden (wenn nicht sogar zu verbieten).
Dan Albert
6
@Mehrdad: Warum sich mit ausreichend zufrieden geben, wenn man großartig haben kann? Ich würde sicher nicht in Blub programmieren wollen , selbst wenn es ausreichend wäre. Das heißt nicht, dass dies insbesondere für dieses Beispiel der Fall ist, aber als jemand, der die meiste Zeit damit verbringt, aus Kompatibilitätsgründen in C89 zu programmieren, ist dies definitiv ein echtes Problem.
Dan Albert
3
Ich denke nicht, dass dies wirklich eine Antwort auf die Frage ist. Für mich fragt er nach Anwendungsfällen, bei denen dies push_backvorzuziehen ist.
Mr. Boy
3
@ Mr.Boy: Es ist vorzuziehen, wenn Sie mit Pre-C ++ 11-Compilern abwärtskompatibel sein möchten. War das in meiner Antwort unklar?
user541686
6
Dies hat viel mehr Aufmerksamkeit erhalten als ich erwartet hatte, also für alle, die dies lesen: emplace_backist keine "großartige" Version von push_back. Es ist eine potenziell gefährliche Version davon. Lesen Sie die anderen Antworten.
user541686
68

Einige Bibliotheksimplementierungen von emplace_back verhalten sich nicht wie im C ++ - Standard angegeben, einschließlich der Version, die mit Visual Studio 2012, 2013 und 2015 geliefert wird.

Um bekannte Compilerfehler zu std::vector::push_back()berücksichtigen , verwenden Sie lieber, wenn die Parameter auf Iteratoren oder andere Objekte verweisen, die nach dem Aufruf ungültig werden.

std::vector<int> v;
v.emplace_back(123);
v.emplace_back(v[0]); // Produces incorrect results in some compilers

Auf einem Compiler enthält v die Werte 123 und 21 anstelle der erwarteten Werte 123 und 123. Dies liegt an der Tatsache, dass der zweite Aufruf von emplace_backzu einer Größenänderung führt, bei der der Punkt v[0]ungültig wird.

Eine funktionierende Implementierung des obigen Codes würde push_back()statt emplace_back()wie folgt verwendet:

std::vector<int> v;
v.emplace_back(123);
v.push_back(v[0]);

Hinweis: Die Verwendung eines Inte-Vektors dient zu Demonstrationszwecken. Ich entdeckte dieses Problem mit einer viel komplexeren Klasse, die dynamisch zugewiesene Mitgliedsvariablen und den Aufruf enthielt, emplace_back()zu einem harten Absturz zu führen.

Marc
quelle
Warten. Dies scheint ein Fehler zu sein. Wie kann push_backdas in diesem Fall anders sein?
Balki
12
Der Aufruf von emplace_back () verwendet eine perfekte Weiterleitung, um die Konstruktion an Ort und Stelle durchzuführen, und als solche wird v [0] erst ausgewertet, nachdem die Größe des Vektors geändert wurde (an diesem Punkt ist v [0] ungültig). push_back erstellt das neue Element und kopiert / verschiebt das Element nach Bedarf, und v [0] wird vor jeder Neuzuweisung ausgewertet.
Marc
1
@Marc: Der Standard garantiert, dass emplace_back auch für Elemente innerhalb des Bereichs funktioniert.
David Stone
1
@DavidStone: Könnten Sie bitte einen Hinweis darauf geben, wo im Standard dieses Verhalten garantiert ist? In beiden Fällen weisen Visual Studio 2012 und 2015 ein falsches Verhalten auf.
Marc
4
@cameino: emplace_back verzögert die Auswertung seines Parameters, um unnötiges Kopieren zu reduzieren. Das Verhalten ist entweder undefiniert oder ein Compiler-Fehler (ausstehende Analyse des Standards). Ich habe kürzlich den gleichen Test gegen Visual Studio 2015 durchgeführt und 123,3 unter Release x64, 123,40 unter Release Win32 und 123, -572662307 unter Debug x64 und Debug Win32 erhalten.
Marc