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?
c++
c++11
move-semantics
user2030677
quelle
quelle
Antworten:
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::string
hat 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.
Weil dies es unmöglich machen würde, Werte zu übergeben, wie in:
Wenn
S
nur ein Konstruktor vorhanden wäre, der r-Werte akzeptiert, würde das oben Gesagte nicht kompiliert.Wenn Sie einen R - Wert übergeben, wird diese bewegt in
str
, und das wird schließlich in verschoben werdendata
. Es wird kein Kopieren durchgeführt. Wenn Sie einen L - Wert, auf der anderen Seite übergeben, wird der L - Wert wird kopiert instr
, und dann zog indata
.Um es zusammenzufassen, zwei Züge für rWerte, eine Kopie und ein Zug für lWerte.
Erstens ist, wie oben erwähnt, die erste nicht immer eine Kopie; und dies sagte, die Antwort lautet: " Weil es effizient (Bewegungen von
std::string
Objekten 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ättenconst
).Dies bedeutet, dass das Aufnehmen nach Wert genauso gut ist wie das Aufnehmen nach l-Wert,
const
wenn 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.
quelle
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.data
Mitglied behalten möchten )? Sie würden eine Kopie haben, selbst wenn Sie unter Bezugnahme aufconst
data
!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&
:In diesem Fall wird immer eine einzelne Kopie ausgeführt. Wenn Sie aus einer rohen C-Zeichenfolge
std::string
erstellen, wird a erstellt und dann erneut kopiert: zwei Zuordnungen.Es gibt die C ++ 03-Methode, einen Verweis auf a zu nehmen
std::string
und ihn dann in einen lokalen zu tauschenstd::string
:Das ist die C ++ 03-Version von "Verschiebungssemantik" und
swap
kann oft so optimiert werden, dass sie sehr billig ist (ähnlich wie bei amove
). Es sollte auch im Kontext analysiert werden:und zwingt Sie, ein nicht temporäres zu bilden
std::string
und es dann zu verwerfen. (Ein temporäres Objektstd::string
kann 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 mitstd::move
oder 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.Verwenden:
Als nächstes können wir die vollständige C ++ 11-Version erstellen, die sowohl das Kopieren als auch Folgendes unterstützt
move
:Wir können dann untersuchen, wie dies verwendet wird:
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:
in jedem dieser Szenarien:
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 Extracopy
.Wenn wir also davon ausgehen, dass dies
move
billig 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
move
Sekunden 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
throw
s aus ihrem Körper in den aufrufenden Bereich verschieben (Wer kann es manchmal durch direkte Konstruktion vermeiden oder die Elemente undmove
in das Argument einbauen, um zu steuern, wo das Werfen stattfindet). Es lohnt sich oft, Methoden zu machen, die nicht geworfen werden.quelle
noexcept
. Indem Sie Daten kopieren, können Sie Ihre Funktion erstellennoexcept
und jede durch Kopierkonstruktion verursachte potenzielle Auslösung (z. B. zu wenig Speicher) außerhalb Ihres Funktionsaufrufs verursachen.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).
quelle
Sie möchten sich nicht wiederholen, indem Sie einen Konstruktor für den Umzug und einen für die Kopie schreiben:
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:
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.
quelle