Warum kopieren wir dann umziehen?

98

Ich habe irgendwo Code gesehen, in dem jemand beschlossen hat, ein Objekt zu kopieren und es anschließend in ein Datenelement einer Klasse zu verschieben. Dies ließ mich insofern verwirrt, als ich dachte, der springende Punkt beim Bewegen sei, das Kopieren zu vermeiden. Hier ist das Beispiel:

struct S
{
    S(std::string str) : data(std::move(str))
    {}
};

Hier sind meine Fragen:

  • Warum nehmen wir keinen r-Wert-Verweis auf str?
  • Wird eine Kopie nicht teuer sein, besonders wenn man so etwas bedenkt std::string?
  • Was wäre der Grund für den Autor, sich für eine Kopie und dann für einen Umzug zu entscheiden?
  • Wann soll ich das selbst machen?
user2030677
quelle
sieht für mich wie ein dummer Fehler aus, aber ich bin gespannt, ob jemand mit mehr Wissen zu diesem Thema etwas dazu zu sagen hat.
Dave
Diese Fragen und Antworten, die ich ursprünglich vergessen habe zu verlinken, sind möglicherweise auch für das Thema relevant.
Andy Prowl

Antworten:

97

Bevor ich Ihre Fragen beantworte, scheint eine Sache falsch zu sein: Die Wertschöpfung in C ++ 11 bedeutet nicht immer Kopieren. Wenn ein r-Wert übergeben wird, wird dieser verschoben (sofern ein funktionsfähiger Verschiebungskonstruktor vorhanden ist), anstatt kopiert zu werden. Und std::stringhat einen Verschiebungskonstruktor.

Anders als in C ++ 03 ist es in C ++ 11 oft idiomatisch, Parameter nach Wert zu nehmen, aus den Gründen, die ich unten erläutern werde. In diesen Fragen und Antworten zu StackOverflow finden Sie allgemeinere Richtlinien zum Akzeptieren von Parametern.

Warum nehmen wir keinen r-Wert-Verweis auf str?

Weil dies es unmöglich machen würde, Werte zu übergeben, wie in:

std::string s = "Hello";
S obj(s); // s is an lvalue, this won't compile!

Wenn Snur ein Konstruktor vorhanden wäre, der r-Werte akzeptiert, würde das oben Gesagte nicht kompiliert.

Wird eine Kopie nicht teuer sein, besonders wenn man so etwas bedenkt std::string?

Wenn Sie einen R - Wert übergeben, wird diese bewegt in str, und das wird schließlich in verschoben werden data. Es wird kein Kopieren durchgeführt. Wenn Sie einen L - Wert, auf der anderen Seite übergeben, wird der L - Wert wird kopiert in str, und dann zog in data.

Um es zusammenzufassen, zwei Züge für rWerte, eine Kopie und ein Zug für lWerte.

Was wäre der Grund für den Autor, sich für eine Kopie und dann für einen Umzug zu entscheiden?

Erstens ist, wie oben erwähnt, die erste nicht immer eine Kopie; und dies sagte, die Antwort lautet: " Weil es effizient (Bewegungen von std::stringObjekten sind billig) und einfach ist ".

Unter der Annahme, dass Bewegungen billig sind (SSO hier ignoriert), können sie bei der Betrachtung der Gesamteffizienz dieses Entwurfs praktisch ignoriert werden. Wenn wir dies tun, haben wir eine Kopie für l-Werte (wie wir es hätten, wenn wir einen l-Wert-Verweis auf akzeptiert hätten const) und keine Kopien für r-Werte (während wir immer noch eine Kopie hätten, wenn wir einen l-Wert-Verweis auf akzeptiert hätten const).

Dies bedeutet, dass das Aufnehmen nach Wert genauso gut ist wie das Aufnehmen nach l-Wert, constwenn l-Werte bereitgestellt werden, und besser, wenn r-Werte bereitgestellt werden.

PS: Um einen gewissen Kontext zu bieten, glaube ich, dass dies die Fragen und Antworten sind, auf die sich das OP bezieht.

Andy Prowl
quelle
2
Erwähnenswert ist, dass es sich um ein C ++ 11-Muster handelt, das das const T&Übergeben von Argumenten ersetzt : Im schlimmsten Fall (lvalue) ist dies dasselbe, aber im Falle eines temporären Ereignisses müssen Sie nur das temporäre verschieben. Win-Win.
Syam
3
@ user2030677: An dieser Kopie führt kein Weg vorbei, es sei denn, Sie speichern eine Referenz.
Benjamin Lindley
5
@ user2030677: Wen interessiert es, wie teuer die Kopie ist, solange Sie sie benötigen (und Sie tun dies, wenn Sie eine Kopie in Ihrem dataMitglied behalten möchten )? Sie würden eine Kopie haben, selbst wenn Sie unter Bezugnahme aufconst
Andy Prowl
3
@BenjaminLindley: Vorab schrieb ich: " Unter der Annahme, dass Bewegungen billig sind, können sie bei der Betrachtung der Gesamteffizienz dieses Entwurfs praktisch ignoriert werden. " Ja, es würde den Aufwand für einen Umzug bedeuten, aber dies sollte als vernachlässigbar angesehen werden, es sei denn, es gibt Beweise dafür, dass dies ein echtes Problem ist, das es rechtfertigt, ein einfaches Design in etwas Effizienteres umzuwandeln.
Andy Prowl
1
@ user2030677: Aber das ist ein ganz anderes Beispiel. In dem Beispiel aus Ihrer Frage haben Sie immer eine Kopie dabei data!
Andy Prowl
51

Um zu verstehen, warum dies ein gutes Muster ist, sollten wir die Alternativen sowohl in C ++ 03 als auch in C ++ 11 untersuchen.

Wir haben die C ++ 03-Methode, um eine std::string const&:

struct S
{
  std::string data; 
  S(std::string const& str) : data(str)
  {}
};

In diesem Fall wird immer eine einzelne Kopie ausgeführt. Wenn Sie aus einer rohen C-Zeichenfolge std::stringerstellen, wird a erstellt und dann erneut kopiert: zwei Zuordnungen.

Es gibt die C ++ 03-Methode, einen Verweis auf a zu nehmen std::stringund ihn dann in einen lokalen zu tauschen std::string:

struct S
{
  std::string data; 
  S(std::string& str)
  {
    std::swap(data, str);
  }
};

Das ist die C ++ 03-Version von "Verschiebungssemantik" und swapkann oft so optimiert werden, dass sie sehr billig ist (ähnlich wie bei a move). Es sollte auch im Kontext analysiert werden:

S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal

und zwingt Sie, ein nicht temporäres zu bilden std::stringund es dann zu verwerfen. (Ein temporäres Objekt std::stringkann nicht an eine nicht konstante Referenz gebunden werden.) Es wird jedoch nur eine Zuordnung vorgenommen. Die C ++ 11-Version benötigt a &&und erfordert, dass Sie es mit std::moveoder mit einem temporären Aufruf aufrufen. Dies erfordert, dass der Aufrufer explizit eine Kopie außerhalb des Aufrufs erstellt und diese Kopie in die Funktion oder den Konstruktor verschiebt.

struct S
{
  std::string data; 
  S(std::string&& str): data(std::move(str))
  {}
};

Verwenden:

S tmp("foo"); // legal
std::string s("foo");
S tmp2(std::move(s)); // legal

Als nächstes können wir die vollständige C ++ 11-Version erstellen, die sowohl das Kopieren als auch Folgendes unterstützt move:

struct S
{
  std::string data; 
  S(std::string const& str) : data(str) {} // lvalue const, copy
  S(std::string && str) : data(std::move(str)) {} // rvalue, move
};

Wir können dann untersuchen, wie dies verwendet wird:

S tmp( "foo" ); // a temporary `std::string` is created, then moved into tmp.data

std::string bar("bar"); // bar is created
S tmp2( bar ); // bar is copied into tmp.data

std::string bar2("bar2"); // bar2 is created
S tmp3( std::move(bar2) ); // bar2 is moved into tmp.data

Es ist ziemlich klar, dass diese 2-Überladungstechnik mindestens genauso effizient ist, wenn nicht sogar effizienter als die beiden oben genannten C ++ 03-Stile. Ich werde diese 2-Overload-Version als die "optimalste" Version bezeichnen.

Jetzt untersuchen wir die Version zum Kopieren:

struct S2 {
  std::string data;
  S2( std::string arg ):data(std::move(x)) {}
};

in jedem dieser Szenarien:

S2 tmp( "foo" ); // a temporary `std::string` is created, moved into arg, then moved into S2::data

std::string bar("bar"); // bar is created
S2 tmp2( bar ); // bar is copied into arg, then moved into S2::data

std::string bar2("bar2"); // bar2 is created
S2 tmp3( std::move(bar2) ); // bar2 is moved into arg, then moved into S2::data

Wenn Sie diese Seite an Seite mit der "optimalsten" Version vergleichen, machen wir genau eine zusätzliche move! Nicht ein einziges Mal machen wir ein Extra copy.

Wenn wir also davon ausgehen, dass dies movebillig ist, bietet diese Version fast die gleiche Leistung wie die optimalste Version, jedoch zweimal weniger Code.

Und wenn Sie beispielsweise 2 bis 10 Argumente verwenden, ist die Reduzierung des Codes exponentiell - 2x weniger mit 1 Argument, 4x mit 2, 8x mit 3, 16x mit 4, 1024x mit 10 Argumenten.

Jetzt können wir dies durch perfekte Weiterleitung und SFINAE umgehen. So können Sie einen einzelnen Konstruktor oder eine einzelne Funktionsvorlage schreiben, die 10 Argumente akzeptiert, SFINAE ausführen, um sicherzustellen, dass die Argumente vom richtigen Typ sind, und sie dann in das Verzeichnis verschieben oder kopieren lokaler Staat nach Bedarf. Dies verhindert zwar das tausendfache Problem der Programmgröße, es kann jedoch immer noch eine ganze Reihe von Funktionen aus dieser Vorlage generiert werden. (Instanziierungen von Vorlagenfunktionen erzeugen Funktionen)

Viele generierte Funktionen bedeuten eine größere Größe des ausführbaren Codes, was die Leistung selbst verringern kann.

Für die Kosten von ein paar moveSekunden erhalten wir kürzeren Code und nahezu die gleiche Leistung und sind oft einfacher zu verstehen.

Dies funktioniert nur, weil wir wissen, wenn die Funktion (in diesem Fall ein Konstruktor) aufgerufen wird, dass wir eine lokale Kopie dieses Arguments benötigen. Die Idee ist, dass wir, wenn wir wissen, dass wir eine Kopie erstellen werden, den Anrufer wissen lassen sollten, dass wir eine Kopie erstellen, indem wir sie in unsere Argumentliste aufnehmen. Sie können dann optimieren, dass sie uns eine Kopie geben (indem sie beispielsweise auf unser Argument eingehen).

Ein weiterer Vorteil der "Take by Value" -Technik besteht darin, dass Verschiebungskonstruktoren häufig keine Ausnahme sind. Dies bedeutet, dass die Funktionen, die nach Wert nehmen und aus ihrem Argument herausgehen, häufig keine Ausnahme sein können und alle throws aus ihrem Körper in den aufrufenden Bereich verschieben (Wer kann es manchmal durch direkte Konstruktion vermeiden oder die Elemente und movein das Argument einbauen, um zu steuern, wo das Werfen stattfindet). Es lohnt sich oft, Methoden zu machen, die nicht geworfen werden.

Yakk - Adam Nevraumont
quelle
Ich würde auch hinzufügen, wenn wir wissen, dass wir eine Kopie erstellen werden, sollten wir den Compiler dies tun lassen, da der Compiler es immer besser weiß.
Rayniery
6
Seit ich dies geschrieben habe, wurde mir ein weiterer Vorteil aufgezeigt: Oft können Kopierkonstruktoren werfen, während Verschiebungskonstruktoren oft sind noexcept. Indem Sie Daten kopieren, können Sie Ihre Funktion erstellen noexceptund jede durch Kopierkonstruktion verursachte potenzielle Auslösung (z. B. zu wenig Speicher) außerhalb Ihres Funktionsaufrufs verursachen.
Yakk - Adam Nevraumont
Warum benötigen Sie die Version "lvalue non-const, copy" in der 3-Überladungstechnik? Behandelt der "lvalue const, copy" nicht auch den Nicht-const-Fall?
Bruno Martinez
@ BrunoMartinez wir nicht!
Yakk - Adam Nevraumont
13

Dies ist wahrscheinlich beabsichtigt und ähnelt der Kopier- und Tauschsprache . Da die Zeichenfolge vor dem Konstruktor kopiert wird, ist der Konstruktor selbst ausnahmesicher, da er nur die temporäre Zeichenfolge str austauscht (verschiebt).

Joe
quelle
+1 für die Copy-and-Swap-Parallele. In der Tat hat es viele Ähnlichkeiten.
Syam
11

Sie möchten sich nicht wiederholen, indem Sie einen Konstruktor für den Umzug und einen für die Kopie schreiben:

S(std::string&& str) : data(std::move(str)) {}
S(const std::string& str) : data(str) {}

Dies ist viel Code, insbesondere wenn Sie mehrere Argumente haben. Ihre Lösung vermeidet diese Doppelarbeit bei den Kosten eines unnötigen Umzugs. (Die Umzugsoperation sollte jedoch recht billig sein.)

Die konkurrierende Redewendung ist die perfekte Weiterleitung:

template <typename T>
S(T&& str) : data(std::forward<T>(str)) {}

Die Vorlagenmagie kann je nach übergebenem Parameter verschoben oder kopiert werden. Sie wird im Wesentlichen auf die erste Version erweitert, in der beide Konstruktoren von Hand geschrieben wurden. Hintergrundinformationen finden Sie in Scott Meyers Beitrag zu universellen Referenzen .

Unter Leistungsaspekten ist die perfekte Weiterleitungsversion Ihrer Version überlegen, da unnötige Bewegungen vermieden werden. Man kann jedoch argumentieren, dass Ihre Version leichter zu lesen und zu schreiben ist. Die möglichen Auswirkungen auf die Leistung sollten in den meisten Situationen sowieso keine Rolle spielen, daher scheint es am Ende eine Frage des Stils zu sein.

Philipp Claßen
quelle