Ich habe gerade das Podcast-Interview mit Scott Meyers über C ++ 0x gehört . Die meisten neuen Funktionen haben für mich Sinn gemacht, und ich freue mich jetzt tatsächlich auf C ++ 0x, mit einer Ausnahme. Ich bekomme immer noch keine Bewegungssemantik ... Was ist das genau?
c++
c++-faq
c++11
move-semantics
Dicroce
quelle
quelle
Antworten:
Ich finde es am einfachsten, die Bewegungssemantik mit Beispielcode zu verstehen. Beginnen wir mit einer sehr einfachen Zeichenfolgenklasse, die nur einen Zeiger auf einen Heap-zugewiesenen Speicherblock enthält:
Da wir die Erinnerung selbst verwalten gewählt haben, müssen wir die folgen Regeldetri . Ich werde das Schreiben des Zuweisungsoperators verschieben und vorerst nur den Destruktor und den Kopierkonstruktor implementieren:
Der Kopierkonstruktor definiert, was das Kopieren von Zeichenfolgenobjekten bedeutet. Der Parameter ist
const string& that
an alle Ausdrücke vom Typ string gebunden, mit denen Sie in den folgenden Beispielen Kopien erstellen können:Jetzt kommt der Schlüsseleinblick in die Bewegungssemantik. Beachten Sie, dass nur in der ersten Zeile, in der wir kopieren,
x
diese tiefe Kopie wirklich notwendig ist, da wir sie möglicherweisex
später überprüfen möchten und sehr überrascht wären, wennx
sie sich irgendwie geändert hätte. Haben Sie bemerkt, dass ich geradex
dreimal gesagt habe (viermal, wenn Sie diesen Satz einfügen) und jedes Mal genau dasselbe Objekt gemeint habe ? Wir nennen Ausdrücke wiex
"lWerte".Die Argumente in den Zeilen 2 und 3 sind keine l-Werte, sondern r-Werte, da die zugrunde liegenden Zeichenfolgenobjekte keine Namen haben und der Client sie zu einem späteren Zeitpunkt nicht mehr überprüfen kann. r-Werte bezeichnen temporäre Objekte, die beim nächsten Semikolon zerstört werden (genauer gesagt: am Ende des vollständigen Ausdrucks, der den r-Wert lexikalisch enthält). Dies ist wichtig, da wir während der Initialisierung von
b
undc
mit der Quellzeichenfolge tun konnten, was wir wollten, und der Client keinen Unterschied feststellen konnte !C ++ 0x führt einen neuen Mechanismus namens "rvalue reference" ein, der es uns unter anderem ermöglicht, rvalue-Argumente durch Funktionsüberladung zu erkennen. Wir müssen lediglich einen Konstruktor mit einem rvalue-Referenzparameter schreiben. Innerhalb dieses Konstruktors können wir mit der Quelle alles tun, was wir wollen , solange wir sie in einem gültigen Zustand belassen:
Was haben wir hier gemacht? Anstatt die Heap-Daten gründlich zu kopieren, haben wir nur den Zeiger kopiert und dann den ursprünglichen Zeiger auf null gesetzt (um zu verhindern, dass 'delete []' vom Destruktor des Quellobjekts unsere 'gerade gestohlenen Daten' freigibt). Tatsächlich haben wir die Daten "gestohlen", die ursprünglich zur Quellzeichenfolge gehörten. Die wichtigste Erkenntnis ist wiederum, dass der Client unter keinen Umständen feststellen konnte, dass die Quelle geändert wurde. Da wir hier nicht wirklich eine Kopie erstellen, nennen wir diesen Konstruktor einen "Verschiebungskonstruktor". Seine Aufgabe ist es, Ressourcen von einem Objekt zu einem anderen zu verschieben, anstatt sie zu kopieren.
Herzlichen Glückwunsch, Sie verstehen jetzt die Grundlagen der Bewegungssemantik! Fahren wir mit der Implementierung des Zuweisungsoperators fort. Wenn Sie mit dem Kopier- und Austausch-Idiom nicht vertraut sind , lernen Sie es und kehren Sie zurück, da es sich um ein fantastisches C ++ - Idiom handelt, das sich auf die Ausnahmesicherheit bezieht.
Huh, das ist es? "Wo ist die rWertreferenz?" du könntest fragen. "Wir brauchen es hier nicht!" ist meine Antwort :)
Beachten Sie, dass wir den Parameter als
that
Wert übergeben und daherthat
wie jedes andere Zeichenfolgenobjekt initialisiert werden müssen. Wie genau wirdthat
es initialisiert? In den alten Tagen von C ++ 98 wäre die Antwort "vom Kopierkonstruktor" gewesen. In C ++ 0x wählt der Compiler zwischen dem Kopierkonstruktor und dem Verschiebungskonstruktor basierend darauf, ob das Argument für den Zuweisungsoperator ein l-Wert oder ein r-Wert ist.Wenn Sie also sagen
a = b
, wird der Kopierkonstruktor initialisiertthat
(da der Ausdruckb
ein Wert ist), und der Zuweisungsoperator tauscht den Inhalt gegen eine frisch erstellte, tiefe Kopie aus. Das ist genau die Definition der Kopie und der Austauschsprache - erstellen Sie eine Kopie, tauschen Sie den Inhalt mit der Kopie aus und entfernen Sie die Kopie, indem Sie den Bereich verlassen. Hier gibt es nichts Neues.Wenn Sie jedoch sagen
a = x + y
, wird der Verschiebungskonstruktor initialisiertthat
(da der Ausdruckx + y
ein r-Wert ist), sodass keine tiefe Kopie erforderlich ist, sondern nur eine effiziente Verschiebung.that
ist immer noch ein unabhängiges Objekt vom Argument, aber seine Konstruktion war trivial, da die Heap-Daten nicht kopiert, sondern nur verschoben werden mussten. Es war nicht notwendig, es zu kopieren, dax + y
es sich um einen r-Wert handelt. Auch hier ist es in Ordnung, von Zeichenfolgenobjekten zu wechseln, die mit r-Werten gekennzeichnet sind.Zusammenfassend lässt sich sagen, dass der Kopierkonstruktor eine tiefe Kopie erstellt, da die Quelle unberührt bleiben muss. Der Verschiebungskonstruktor hingegen kann den Zeiger einfach kopieren und dann den Zeiger in der Quelle auf null setzen. Es ist in Ordnung, das Quellobjekt auf diese Weise "aufzuheben", da der Client keine Möglichkeit hat, das Objekt erneut zu untersuchen.
Ich hoffe, dieses Beispiel hat den Hauptpunkt vermittelt. Es gibt noch viel mehr, Referenzen zu bewerten und Semantik zu verschieben, was ich absichtlich weggelassen habe, um es einfach zu halten. Wenn Sie weitere Informationen wünschen, lesen Sie bitte meine ergänzende Antwort .
quelle
that.data = 0
würden die Charaktere viel zu früh (wenn der Temporäre stirbt) und auch zweimal zerstört. Sie möchten die Daten stehlen, nicht teilen!delete[]
auf einem Nullptr wird vom C ++ - Standard als No-Op definiert.Meine erste Antwort war eine extrem vereinfachte Einführung in die Verschiebungssemantik, und viele Details wurden absichtlich weggelassen, um sie einfach zu halten. Es gibt jedoch noch viel mehr, um die Semantik zu verschieben, und ich dachte, es wäre Zeit für eine zweite Antwort, um die Lücken zu füllen. Die erste Antwort ist schon ziemlich alt und es fühlte sich nicht richtig an, sie einfach durch einen völlig anderen Text zu ersetzen. Ich denke, es dient immer noch als erste Einführung. Aber wenn Sie tiefer graben wollen, lesen Sie weiter :)
Stephan T. Lavavej hat sich die Zeit genommen, wertvolles Feedback zu geben. Vielen Dank, Stephan!
Einführung
Durch die Verschiebungssemantik kann ein Objekt unter bestimmten Bedingungen die externen Ressourcen eines anderen Objekts übernehmen. Dies ist in zweierlei Hinsicht wichtig:
Aus teuren Kopien billige Züge machen. Ein Beispiel finden Sie in meiner ersten Antwort. Beachten Sie, dass die Verschiebungssemantik keine Vorteile gegenüber der Kopiersemantik bietet, wenn ein Objekt nicht mindestens eine externe Ressource verwaltet (entweder direkt oder indirekt über seine Mitgliedsobjekte). In diesem Fall bedeutet das Kopieren eines Objekts und das Verschieben eines Objekts genau dasselbe:
Implementierung sicherer "Nur-Verschieben" -Typen; Das heißt, Typen, für die das Kopieren keinen Sinn macht, das Verschieben jedoch. Beispiele hierfür sind Sperren, Dateihandles und intelligente Zeiger mit eindeutiger Besitzersemantik. Hinweis: In dieser Antwort
std::auto_ptr
wird eine veraltete C ++ 98-Standardbibliotheksvorlage erläutert , diestd::unique_ptr
in C ++ 11 durch ersetzt wurde. Fortgeschrittene C ++ - Programmierer sind wahrscheinlich zumindest einigermaßen vertrautstd::auto_ptr
, und aufgrund der angezeigten "Verschiebungssemantik" scheint dies ein guter Ausgangspunkt für die Erörterung der Verschiebungssemantik in C ++ 11 zu sein. YMMV.Was ist ein Zug?
Die C ++ 98-Standardbibliothek bietet einen intelligenten Zeiger mit einer eindeutigen Besitzersemantik namens
std::auto_ptr<T>
. Falls Sie nicht vertraut sindauto_ptr
, soll damit sichergestellt werden, dass ein dynamisch zugewiesenes Objekt auch bei Ausnahmen immer freigegeben wird:Das Ungewöhnliche
auto_ptr
ist das "Kopieren":Beachten Sie, wie die Initialisierung
b
mita
sich nicht auf das Dreieck kopieren, sondern überträgt das Eigentum des Dreiecks ausa
zub
. Wir sagen auch "a
wird verschoben inb
" oder "das Dreieck wird verschoben vona
nachb
". Dies mag verwirrend klingen, da das Dreieck selbst immer an derselben Stelle im Speicher bleibt.Der Kopierkonstruktor von
auto_ptr
sieht wahrscheinlich ungefähr so aus (etwas vereinfacht):Gefährliche und harmlose Bewegungen
Das Gefährliche daran
auto_ptr
ist, dass das, was syntaktisch wie eine Kopie aussieht, tatsächlich ein Zug ist. Wenn Sie versuchen, eine Mitgliedsfunktion für ein Verschieben vonauto_ptr
aufzurufen, wird ein undefiniertes Verhalten ausgelöst. Sie müssen daher sehr vorsichtig sein, um keine zu verwenden,auto_ptr
nachdem sie verschoben wurde von:Ist
auto_ptr
aber nicht immer gefährlich. Werksfunktionen sind ein perfekter Anwendungsfall fürauto_ptr
:Beachten Sie, wie beide Beispiele demselben syntaktischen Muster folgen:
Und doch ruft einer von ihnen undefiniertes Verhalten hervor, während der andere dies nicht tut. Was ist also der Unterschied zwischen den Ausdrücken
a
undmake_triangle()
? Sind sie nicht beide vom gleichen Typ? Sie sind es zwar, aber sie haben unterschiedliche Wertkategorien .Wertekategorien
Offensichtlich muss es einen tiefgreifenden Unterschied zwischen dem Ausdruck,
a
der eineauto_ptr
Variable bezeichnet, und dem Ausdruck geben,make_triangle()
der den Aufruf einer Funktion bezeichnet, die einenauto_ptr
by-Wert zurückgibt , wodurch beiauto_ptr
jedem Aufruf ein neues temporäres Objekt erstellt wird.a
ist ein Beispiel für einen l-Wert , währendmake_triangle()
es ein Beispiel für einen r-Wert ist .Das Verschieben von Werten wie
a
ist gefährlich, da wir später versuchen könnten, eine Mitgliedsfunktion übera
aufzurufen und undefiniertes Verhalten aufzurufen. Auf der anderen Seite ist es absolutmake_triangle()
sicher, von r-Werten zu wechseln , da dies absolut sicher ist, nachdem der Kopierkonstruktor seine Arbeit erledigt hat. Es gibt keinen Ausdruck, der das Temporäre bezeichnet; Wenn wir einfach nochmake_triangle()
einmal schreiben , erhalten wir eine andere temporäre. Tatsächlich ist das vorübergehende Verschieben bereits in der nächsten Zeile verschwunden:Beachten Sie, dass die Buchstaben
l
undr
einen historischen Ursprung auf der linken und rechten Seite einer Aufgabe haben. Dies gilt in C ++ nicht mehr, da es lWerte gibt, die nicht auf der linken Seite einer Zuweisung angezeigt werden können (wie Arrays oder benutzerdefinierte Typen ohne Zuweisungsoperator), und es gibt rWerte, die dies können (alle rWerte von Klassentypen) mit einem Zuweisungsoperator).RWertreferenzen
Wir verstehen jetzt, dass das Bewegen von l-Werten potenziell gefährlich ist, aber das Bewegen von r-Werten harmlos ist. Wenn C ++ Sprachunterstützung hätte, um lvalue-Argumente von rvalue-Argumenten zu unterscheiden, könnten wir entweder das Verschieben von lvalues vollständig verbieten oder zumindest das Verschieben von lvalues am Aufrufstandort explizit machen , damit wir uns nicht mehr versehentlich bewegen.
Die Antwort von C ++ 11 auf dieses Problem lautet rWertreferenzen . Eine rvalue-Referenz ist eine neue Art von Referenz, die nur an rvalues bindet, und die Syntax lautet
X&&
. Die gute alte ReferenzX&
ist jetzt als Wertreferenz bekannt . (Beachten Sie, dassX&&
ist nicht ein Verweis auf eine Referenz, es gibt nicht so etwas in C ++.)Wenn wir
const
in die Mischung werfen , haben wir bereits vier verschiedene Arten von Referenzen. An welche Arten von TypausdrückenX
können sie binden?In der Praxis können Sie vergessen
const X&&
. Es ist nicht sehr nützlich, sich auf das Lesen von Werten zu beschränken.Implizite Konvertierungen
R-Wert-Referenzen durchliefen mehrere Versionen. Seit Version 2.1
X&&
bindet eine rvalue-Referenz auch an alle Wertekategorien eines anderen TypsY
, sofern eine implizite Konvertierung vonY
nach erfolgtX
. In diesem Fall wird ein temporärer TypX
erstellt, und die r-Wert-Referenz ist an diesen temporären Typ gebunden:Im obigen Beispiel
"hello world"
ist ein Wert vom Typconst char[12]
. Da es eine implizite Konvertierung vonconst char[12]
bisconst char*
nach gibtstd::string
, wird ein temporäres vom Typstd::string
erstellt undr
an dieses temporäre gebunden. Dies ist einer der Fälle, in denen die Unterscheidung zwischen r-Werten (Ausdrücken) und temporären Werten (Objekten) etwas verschwommen ist.Konstruktoren verschieben
Ein nützliches Beispiel für eine Funktion mit einem
X&&
Parameter ist der VerschiebungskonstruktorX::X(X&& source)
. Der Zweck besteht darin, das Eigentum an der verwalteten Ressource von der Quelle auf das aktuelle Objekt zu übertragen.In C ++ 11
std::auto_ptr<T>
wurde ersetzt,std::unique_ptr<T>
wodurch rvalue-Referenzen genutzt werden. Ich werde eine vereinfachte Version von entwickeln und diskutierenunique_ptr
. Erstens kapseln wir einen Rohzeiger und überlasten die Betreiber->
und*
, so dass unsere Klasse fühlt sich wie ein Zeiger:Der Konstruktor übernimmt das Eigentum an dem Objekt und der Destruktor löscht es:
Nun kommt der interessante Teil, der Verschiebungskonstruktor:
Dieser Verschiebungskonstruktor macht genau das, was der
auto_ptr
Kopierkonstruktor getan hat, kann jedoch nur mit rWerten geliefert werden:Die zweite Zeile kann nicht kompiliert werden, da
a
es sich um einen l-unique_ptr&& source
Wert handelt. Der Parameter kann jedoch nur an r-Werte gebunden werden. Genau das wollten wir; gefährliche Bewegungen sollten niemals implizit sein. Die dritte Zeile wird gut kompiliert, damake_triangle()
es sich um einen Wert handelt. Der Verschiebungskonstruktor überträgt das Eigentum von der temporären aufc
. Auch dies ist genau das, was wir wollten.Zuweisungsoperatoren verschieben
Das letzte fehlende Teil ist der Bewegungszuweisungsoperator. Seine Aufgabe ist es, die alte Ressource freizugeben und die neue Ressource aus ihrem Argument zu erhalten:
Beachten Sie, wie diese Implementierung des Verschiebungszuweisungsoperators die Logik sowohl des Destruktors als auch des Verschiebungskonstruktors dupliziert. Kennen Sie die Copy-and-Swap-Sprache? Es kann auch angewendet werden, um die Semantik als Move-and-Swap-Idiom zu verschieben:
Dies
source
ist eine Variable vom Typunique_ptr
, die vom Verschiebungskonstruktor initialisiert wird. Das heißt, das Argument wird in den Parameter verschoben. Das Argument muss weiterhin ein r-Wert sein, da der Verschiebungskonstruktor selbst einen r-Wert-Referenzparameter hat. Wenn der Kontrollfluss die schließende Klammer von erreichtoperator=
,source
verlässt er den Gültigkeitsbereich und gibt die alte Ressource automatisch frei.Von lWerten weggehen
Manchmal wollen wir uns von lWerten entfernen. Das heißt, manchmal möchten wir, dass der Compiler einen l-Wert so behandelt, als wäre er ein r-Wert, damit er den Verschiebungskonstruktor aufrufen kann, obwohl er möglicherweise unsicher ist. Zu diesem Zweck bietet C ++ 11 eine Standardvorlage für Bibliotheksfunktionen, die
std::move
im Header aufgerufen wird<utility>
. Dieser Name ist etwas unglücklich, weil erstd::move
einfach einen l-Wert in einen r-Wert umwandelt. es ist nicht alles von selbst bewegen. Es ermöglicht lediglich das Bewegen. Vielleicht hätte es benannt werden sollenstd::cast_to_rvalue
oderstd::enable_move
, aber wir bleiben jetzt beim Namen.So bewegen Sie sich explizit von einem Wert:
Beachten Sie, dass nach der dritten Zeile
a
kein Dreieck mehr vorhanden ist. Das ist in Ordnung, denn durch explizites Schreibenstd::move(a)
haben wir unsere Absichten klargestellt: "Lieber Konstruktor, machen Sie, was Sie wollen,a
um zu initialisierenc
; es interessiert mich nichta
mehr. Fühlen Sie sich frei, Ihren Weg mit zu habena
."X-Werte
Beachten Sie, dass
std::move(a)
die Auswertung , obwohl es sich um einen r-Wert handelt, kein temporäres Objekt erstellt. Dieses Rätsel zwang das Komitee, eine dritte Wertekategorie einzuführen. Etwas , das mit einer R - Wert Referenz gebunden werden kann, auch wenn es nicht ein R - Wert im herkömmlichen Sinne ist, ist ein genannt xValue (auslaufend Wert). Die traditionellen Werte wurden in Werte (Reine Werte) umbenannt.Sowohl prvalues als auch xvalues sind rvalues. X- und l-Werte sind beide gl-Werte (Generalized lvalues). Die Zusammenhänge lassen sich mit einem Diagramm leichter erfassen:
Beachten Sie, dass nur x-Werte wirklich neu sind. Der Rest ist nur auf das Umbenennen und Gruppieren zurückzuführen.
Funktionen verlassen
Bisher haben wir eine Bewegung in lokale Variablen und in Funktionsparameter gesehen. Eine Bewegung ist aber auch in die entgegengesetzte Richtung möglich. Wenn eine Funktion nach Wert zurückgegeben wird, wird ein Objekt am Aufrufstandort (wahrscheinlich eine lokale Variable oder ein temporäres Objekt, kann aber eine beliebige Art von Objekt sein) mit dem Ausdruck nach der
return
Anweisung als Argument für den Verschiebungskonstruktor initialisiert :Es ist vielleicht überraschend, dass automatische Objekte (lokale Variablen, die nicht als deklariert sind
static
) auch implizit aus Funktionen verschoben werden können:Wie kommt es, dass der Verschiebungskonstruktor den l-Wert
result
als Argument akzeptiert ? Der Umfang vonresult
ist kurz vor dem Ende und wird beim Abwickeln des Stapels zerstört. Niemand konnte sich danach beschweren, dassresult
sich das irgendwie geändert hatte; Wenn der Kontrollfluss wieder beim Anrufer ist,result
existiert er nicht mehr! Aus diesem Grund verfügt C ++ 11 über eine spezielle Regel, die es ermöglicht, automatische Objekte von Funktionen zurückzugeben, ohne schreiben zu müssenstd::move
. Tatsächlich sollten Sie niemals verwendenstd::move
, um automatische Objekte aus Funktionen zu verschieben, da dies die "Named Return Value Optimization" (NRVO) verhindert.Beachten Sie, dass in beiden Werksfunktionen der Rückgabetyp ein Wert und keine r-Wert-Referenz ist. R-Wert-Referenzen sind immer noch Referenzen, und wie immer sollten Sie niemals eine Referenz auf ein automatisches Objekt zurückgeben. Der Aufrufer würde eine baumelnde Referenz erhalten, wenn Sie den Compiler dazu verleiten würden, Ihren Code wie folgt zu akzeptieren:
Umzug in Mitglieder
Früher oder später werden Sie Code wie folgt schreiben:
Grundsätzlich wird sich der Compiler beschweren, dass dies
parameter
ein Wert ist. Wenn Sie sich den Typ ansehen, sehen Sie eine r-Wert-Referenz, aber eine r-Wert-Referenz bedeutet einfach "eine Referenz, die an einen r-Wert gebunden ist". es bedeutet nicht , dass die Referenz selbst ein Wert ist! In der Tatparameter
ist nur eine gewöhnliche Variable mit einem Namen. Sie könnenparameter
im Body des Konstruktors so oft verwenden, wie Sie möchten, und es wird immer dasselbe Objekt bezeichnet. Es wäre gefährlich, sich implizit davon zu entfernen, daher verbietet es die Sprache.Die Lösung besteht darin, den Umzug manuell zu aktivieren:
Man könnte argumentieren, dass
parameter
das nach der Initialisierung von nicht mehr verwendet wirdmember
. Warum gibt es keine spezielle Regel zum stillen Einfügenstd::move
wie bei Rückgabewerten? Wahrscheinlich, weil es die Compiler-Implementierer zu sehr belasten würde. Was wäre zum Beispiel, wenn sich der Konstruktorkörper in einer anderen Übersetzungseinheit befindet? Im Gegensatz dazu muss die Rückgabewertregel lediglich die Symboltabellen überprüfen, um festzustellen, ob der Bezeichner nach demreturn
Schlüsselwort ein automatisches Objekt bezeichnet oder nicht .Sie können den
parameter
by-Wert auch übergeben. Für Nur-Bewegung-Typen wieunique_ptr
scheint es noch keine etablierte Sprache zu geben. Persönlich übergebe ich lieber den Wert, da dies weniger Unordnung in der Benutzeroberfläche verursacht.Spezielle Mitgliedsfunktionen
C ++ 98 deklariert implizit drei spezielle Elementfunktionen bei Bedarf, dh wenn sie irgendwo benötigt werden: den Kopierkonstruktor, den Kopierzuweisungsoperator und den Destruktor.
R-Wert-Referenzen durchliefen mehrere Versionen. Seit Version 3.0 deklariert C ++ 11 bei Bedarf zwei zusätzliche spezielle Elementfunktionen: den Verschiebungskonstruktor und den Verschiebungszuweisungsoperator. Beachten Sie, dass weder VC10 noch VC11 noch Version 3.0 entsprechen, sodass Sie sie selbst implementieren müssen.
Diese beiden neuen speziellen Elementfunktionen werden nur implizit deklariert, wenn keine der speziellen Elementfunktionen manuell deklariert wird. Wenn Sie Ihren eigenen Verschiebungskonstruktor oder Verschiebungszuweisungsoperator deklarieren, werden weder der Kopierkonstruktor noch der Kopierzuweisungsoperator implizit deklariert.
Was bedeuten diese Regeln in der Praxis?
Beachten Sie, dass der Kopierzuweisungsoperator und der Verschiebungszuweisungsoperator zu einem einzigen einheitlichen Zuweisungsoperator zusammengeführt werden können, wobei das Argument nach Wert sortiert wird:
Auf diese Weise sinkt die Anzahl der zu implementierenden speziellen Elementfunktionen von fünf auf vier. Hier gibt es einen Kompromiss zwischen Ausnahmesicherheit und Effizienz, aber ich bin kein Experte in diesem Bereich.
Weiterleitungsreferenzen ( früher als Universalreferenzen bekannt )
Betrachten Sie die folgende Funktionsvorlage:
Sie können erwarten
T&&
, nur an r-Werte zu binden, da es auf den ersten Blick wie eine r-Wert-Referenz aussieht. Wie sich jedoch herausstellt,T&&
bindet auch an lWerte:Wenn das Argument ein R - Wert vom Typ ist
X
,T
wird abgeleitet zu seinX
, daherT&&
bedeutetX&&
. Das würde jeder erwarten. Wenn es sich bei dem Argument jedoch um einen Wert vom Typ handeltX
, wird aufgrund einer Sonderregel davon ausgegangen, dass dies derT
Fall istX&
, undT&&
würde daher so etwas wie bedeutenX& &&
. Aber da C ++ noch keine Ahnung von Verweisen auf Referenzen hat, die ArtX& &&
ist , kollabiert inX&
. Dies mag zunächst verwirrend und nutzlos klingen, aber das Zusammenfallen von Referenzen ist für eine perfekte Weiterleitung unerlässlich (auf die hier nicht näher eingegangen wird).Wenn Sie eine Funktionsvorlage auf rWerte beschränken möchten, können Sie SFINAE mit Typmerkmalen kombinieren :
Umsetzung des Umzugs
Nachdem Sie nun das Reduzieren von Referenzen verstanden haben, wird Folgendes
std::move
implementiert:Wie Sie sehen können,
move
akzeptiert dank der Weiterleitungsreferenz jede Art von ParameterT&&
und gibt eine rWert-Referenz zurück. Derstd::remove_reference<T>::type
Metafunktionsaufruf ist notwendig, da andernfalls für lWerte vom TypX
der Rückgabetyp wäreX& &&
, der zusammenbrechen würdeX&
. Dat
es sich immer um einen l-Wert handelt (denken Sie daran, dass eine benannte r-Wert-Referenz ein l-Wert ist), wir abert
an eine r-Wert-Referenz binden möchten, müssen wir explizitt
in den richtigen Rückgabetyp umwandeln . Der Aufruf einer Funktion, die eine rvalue-Referenz zurückgibt, ist selbst ein xvalue. Jetzt weißt du woher xvalues kommen;)Beachten Sie, dass die Rückgabe per rvalue-Referenz in diesem Beispiel in Ordnung ist, da
t
dies kein automatisches Objekt bezeichnet, sondern ein Objekt, das vom Aufrufer übergeben wurde.quelle
Die Verschiebungssemantik basiert auf rWertreferenzen .
Ein r-Wert ist ein temporäres Objekt, das am Ende des Ausdrucks zerstört wird. In aktuellem C ++ binden r-Werte nur an
const
Referenzen. C ++ 1x erlaubtconst
buchstabierte Nicht- Wert-ReferenzenT&&
, die Referenzen auf ein Wert-Objekt sind.Da ein r-Wert am Ende eines Ausdrucks sterben wird, können Sie seine Daten stehlen . Anstatt das Kopieren in ein anderes Objekt, Sie bewegen ihre Daten hinein.
In dem obigen Code, mit alten Compiler des Ergebnisses
f()
wird kopiert inx
VerwendungX
‚s Copykonstruktor. Wenn Ihr Compiler die Verschiebungssemantik unterstützt undX
über einen Verschiebungskonstruktor verfügt, wird dieser stattdessen aufgerufen. Da seinrhs
Argument ein Wert ist , wissen wir, dass es nicht mehr benötigt wird und wir können seinen Wert stehlen.Der Wert wird also von dem unbenannten temporären Wert verschoben, der von
f()
bis zurückgegeben wirdx
(während die Daten vonx
, die auf einen leeren Wert initialisiert wurden,X
in den temporären Wert verschoben werden, der nach der Zuweisung zerstört wird).quelle
this->swap(std::move(rhs));
weil benannte rWertreferenzen lWerte sindrhs
ein bisschen falsch, laut @ Tacyts Kommentar: ist ein Wert im Kontext vonX::X(X&& rhs)
. Sie müssen anrufenstd::move(rhs)
, um einen Wert zu erhalten, aber dies macht die Antwort irgendwie umstritten.Angenommen, Sie haben eine Funktion, die ein wesentliches Objekt zurückgibt:
Wenn Sie Code wie folgt schreiben:
Dann erstellt ein gewöhnlicher C ++ - Compiler ein temporäres Objekt für das Ergebnis von
multiply()
, ruft den Kopierkonstruktor zum Initialisieren aufr
und zerstört dann den temporären Rückgabewert. Mit der Verschiebungssemantik in C ++ 0x kann der "Verschiebungskonstruktor" aufgerufen werden, umr
durch Kopieren seines Inhalts zu initialisieren , und der temporäre Wert wird dann verworfen, ohne ihn zerstören zu müssen.Dies ist besonders wichtig, wenn (wie im
Matrix
obigen Beispiel) das zu kopierende Objekt zusätzlichen Speicher auf dem Heap zuweist, um seine interne Darstellung zu speichern. Ein Kopierkonstruktor müsste entweder eine vollständige Kopie der internen Darstellung erstellen oder intern die Referenzzählung und die Semantik des Kopierens beim Schreiben verwenden. Ein Verschiebungskonstruktor würde den Heapspeicher in Ruhe lassen und einfach den Zeiger in dasMatrix
Objekt kopieren .quelle
Wenn Sie wirklich an einer guten, ausführlichen Erklärung der Verschiebungssemantik interessiert sind, empfehle ich dringend, das Originalpapier zu lesen: "Ein Vorschlag, der C ++ - Sprache Unterstützung für Verschiebungssemantik hinzuzufügen."
Es ist sehr leicht zugänglich und leicht zu lesen und ein hervorragendes Argument für die Vorteile, die sie bieten. Es gibt andere neuere und aktuellere Artikel über die Bewegungssemantik auf der WG21-Website , aber dieser ist wahrscheinlich der einfachste, da er sich den Dingen aus einer Top-Level-Sicht nähert und nicht sehr auf die Details der Sprache eingeht.
quelle
Bei der Verschiebungssemantik geht es darum , Ressourcen zu übertragen, anstatt sie zu kopieren, wenn niemand mehr den Quellwert benötigt.
In C ++ 03 werden Objekte häufig kopiert, nur um zerstört oder zugewiesen zu werden, bevor ein Code den Wert erneut verwendet. Wenn Sie beispielsweise einen Wert von einer Funktion zurückgeben - es sei denn, RVO wird aktiviert -, wird der zurückgegebene Wert in den Stapelrahmen des Aufrufers kopiert, verlässt den Gültigkeitsbereich und wird zerstört. Dies ist nur eines von vielen Beispielen: Siehe Pass-by-Value, wenn das Quellobjekt temporär ist, Algorithmen wie
sort
diese ordnen Elemente einfach neu an, ordnen sie neu zu,vector
wenn siecapacity()
überschritten werden usw.Wenn solche Kopier- / Zerstörungspaare teuer sind, liegt dies normalerweise daran, dass das Objekt eine schwere Ressource besitzt.
vector<string>
Kann beispielsweise einen dynamisch zugewiesenen Speicherblock besitzen, der ein Array vonstring
Objekten enthält, von denen jedes seinen eigenen dynamischen Speicher hat. Das Kopieren eines solchen Objekts ist kostspielig: Sie müssen jedem dynamisch zugewiesenen Block in der Quelle neuen Speicher zuweisen und alle Werte kopieren. Dann müssen Sie den gesamten Speicher, den Sie gerade kopiert haben, freigeben. Das Verschieben eines großenvector<string>
Zeigers bedeutet jedoch, dass nur einige Zeiger (die sich auf den dynamischen Speicherblock beziehen) auf das Ziel kopiert und in der Quelle auf Null gesetzt werden.quelle
In einfachen (praktischen) Begriffen:
Das Kopieren eines Objekts bedeutet das Kopieren seiner "statischen" Elemente und das Aufrufen des
new
Operators für seine dynamischen Objekte. Recht?Das Verschieben eines Objekts (ich wiederhole es aus praktischer Sicht) bedeutet jedoch, nur die Zeiger dynamischer Objekte zu kopieren und keine neuen zu erstellen.
Aber ist das nicht gefährlich? Natürlich können Sie ein dynamisches Objekt zweimal zerstören (Segmentierungsfehler). Um dies zu vermeiden, sollten Sie die Quellzeiger "ungültig machen", um zu vermeiden, dass sie zweimal zerstört werden:
Ok, aber wenn ich ein Objekt verschiebe, wird das Quellobjekt unbrauchbar, nein? Natürlich, aber in bestimmten Situationen ist das sehr nützlich. Das offensichtlichste ist, wenn ich eine Funktion mit einem anonymen Objekt aufrufe (temporales, rvalue Objekt, ..., Sie können es mit verschiedenen Namen aufrufen):
In dieser Situation wird ein anonymes Objekt erstellt, als nächstes in den Funktionsparameter kopiert und anschließend gelöscht. Hier ist es also besser, das Objekt zu verschieben, da Sie das anonyme Objekt nicht benötigen und Zeit und Speicher sparen können.
Dies führt zum Konzept einer "rvalue" -Referenz. Sie sind in C ++ 11 nur vorhanden, um festzustellen, ob das empfangene Objekt anonym ist oder nicht. Ich denke, Sie wissen bereits, dass ein "l-Wert" eine zuweisbare Entität ist (der linke Teil des
=
Operators), daher benötigen Sie einen benannten Verweis auf ein Objekt, um als l-Wert fungieren zu können. Ein rWert ist genau das Gegenteil, ein Objekt ohne benannte Referenzen. Aus diesem Grund sind anonymes Objekt und r-Wert Synonyme. Damit:In diesem Fall
A
erstellt der Compiler , wenn ein Objekt vom Typ "kopiert" werden soll, eine l-Wert-Referenz oder eine r-Wert-Referenz, je nachdem, ob das übergebene Objekt benannt ist oder nicht. Wenn nicht, wird Ihr Verschiebungskonstruktor aufgerufen und Sie wissen, dass das Objekt zeitlich begrenzt ist. Sie können seine dynamischen Objekte verschieben, anstatt sie zu kopieren, wodurch Platz und Speicherplatz gespart werden.Es ist wichtig zu beachten, dass "statische" Objekte immer kopiert werden. Es gibt keine Möglichkeit, ein statisches Objekt (Objekt im Stapel und nicht auf dem Heap) zu "verschieben". Daher ist die Unterscheidung "Verschieben" / "Kopieren", wenn ein Objekt keine dynamischen Elemente (direkt oder indirekt) hat, irrelevant.
Wenn Ihr Objekt komplex ist und der Destruktor andere sekundäre Effekte hat, z. B. das Aufrufen der Funktion einer Bibliothek, das Aufrufen anderer globaler Funktionen oder was auch immer, ist es möglicherweise besser, eine Bewegung mit einem Flag zu signalisieren:
Ihr Code ist also kürzer (Sie müssen nicht
nullptr
für jedes dynamische Mitglied eine Zuweisung vornehmen) und allgemeiner.Andere typische Frage: Was ist der Unterschied zwischen
A&&
undconst A&&
? Natürlich können Sie im ersten Fall das Objekt ändern und im zweiten Fall nicht, aber praktische Bedeutung? Im zweiten Fall können Sie es nicht ändern, sodass Sie keine Möglichkeit haben, das Objekt ungültig zu machen (außer mit einem veränderlichen Flag oder ähnlichem), und es gibt keinen praktischen Unterschied zu einem Kopierkonstruktor.Und was ist perfekte Weiterleitung ? Es ist wichtig zu wissen, dass eine "rWertreferenz" eine Referenz auf ein benanntes Objekt im "Bereich des Aufrufers" ist. Im tatsächlichen Bereich ist eine r-Wert-Referenz jedoch ein Name für ein Objekt, sodass sie als benanntes Objekt fungiert. Wenn Sie einen r-Wert-Verweis an eine andere Funktion übergeben, übergeben Sie ein benanntes Objekt, sodass das Objekt nicht wie ein temporäres Objekt empfangen wird.
Das Objekt
a
würde in den aktuellen Parameter von kopiertother_function
. Wenn Sie möchten, dass das Objekta
weiterhin als temporäres Objekt behandelt wird, sollten Sie die folgendestd::move
Funktion verwenden:std::move
Wird mit dieser Zeilea
in einen r-Wert umgewandelt undother_function
erhält das Objekt als unbenanntes Objekt. Wennother_function
keine spezifische Überladung für die Arbeit mit unbenannten Objekten vorliegt, ist diese Unterscheidung natürlich nicht wichtig.Ist das perfekte Weiterleitung? Nicht, aber wir stehen uns sehr nahe. Perfekte Weiterleitung ist nur nützlich, um mit Vorlagen zu arbeiten, mit dem Ziel zu sagen: Wenn ich ein Objekt an eine andere Funktion übergeben muss, muss das Objekt als benanntes Objekt übergeben werden, wenn ich ein benanntes Objekt erhalte, und wenn nicht, Ich möchte es wie ein unbenanntes Objekt übergeben:
Dies ist die Signatur einer prototypischen Funktion, die eine perfekte Weiterleitung verwendet und in C ++ 11 mithilfe von implementiert wurde
std::forward
. Diese Funktion nutzt einige Regeln für die Instanziierung von Vorlagen:Wenn
T
also ein Wert aufA
( T = A &) verweist ,a
auch ( A & && => A &). WennT
ist ein rWert Verweis aufA
,a
auch (A && && => A &&). In beiden Fällena
handelt es sich um ein benanntes Objekt im tatsächlichen Bereich,T
enthält jedoch die Informationen seines "Referenztyps" aus Sicht des Aufruferbereichs. Diese Information (T
) wird als Vorlagenparameter an übergebenforward
und 'a' wird je nach Typ von verschoben oder nichtT
.quelle
Es ist wie eine Kopiersemantik, aber anstatt alle Daten zu duplizieren, müssen Sie die Daten von dem Objekt stehlen, von dem "verschoben" wird.
quelle
Sie wissen, was eine Kopiersemantik bedeutet, oder? Dies bedeutet, dass Sie Typen haben, die kopierbar sind. Für benutzerdefinierte Typen definieren Sie diese entweder explizit, indem Sie einen Kopierkonstruktor und einen Zuweisungsoperator schreiben, oder der Compiler generiert sie implizit. Dadurch wird eine Kopie erstellt.
Die Verschiebungssemantik ist im Grunde ein benutzerdefinierter Typ mit Konstruktor, der eine r-Wert-Referenz (neuer Referenztyp mit && (ja zwei kaufmännische Und-Zeichen)) verwendet, die nicht konstant ist. Dies wird als Verschiebungskonstruktor bezeichnet. Gleiches gilt für den Zuweisungsoperator. Was macht ein Verschiebungskonstruktor? Anstatt Speicher aus seinem Quellargument zu kopieren, verschiebt er Speicher von der Quelle zum Ziel.
Wann möchten Sie das tun? Nun, std :: vector ist ein Beispiel. Angenommen, Sie haben einen temporären std :: vector erstellt und geben ihn von einer Funktion zurück, z. B.:
Wenn die Funktion zurückkehrt, wird der Kopierkonstruktor Overhead haben, wenn (und in C ++ 0x) std :: vector einen Verschiebungskonstruktor hat, anstatt ihn zu kopieren, kann er einfach seine Zeiger setzen und 'Verschieben' dynamisch zuweisen Speicher für die neue Instanz. Es ist eine Art Semantik der Eigentumsübertragung mit std :: auto_ptr.
quelle
Um die Notwendigkeit einer Verschiebungssemantik zu veranschaulichen , betrachten wir dieses Beispiel ohne Verschiebungssemantik:
Hier ist eine Funktion, die ein Objekt vom Typ nimmt
T
und ein Objekt vom gleichen Typ zurückgibtT
:Die oben genannte Funktion verwendet ruft nach Wert , was bedeutet , dass , wenn diese Funktion ein Objekt aufgerufen wird , muss konstruiert von der Funktion verwendet werden.
Da die Funktion auch nach Wert zurückgibt , wird ein weiteres neues Objekt für den Rückgabewert erstellt:
Es wurden zwei neue Objekte erstellt, von denen eines ein temporäres Objekt ist, das nur für die Dauer der Funktion verwendet wird.
Wenn das neue Objekt aus dem Rückgabewert erstellt wird, wird der Kopierkonstruktor aufgerufen, um den Inhalt des temporären Objekts in das neue Objekt zu kopieren . B. Nach Abschluss der Funktion verlässt das in der Funktion verwendete temporäre Objekt den Gültigkeitsbereich und wird zerstört.
Betrachten wir nun, was ein Kopierkonstruktor tut.
Es muss zuerst das Objekt initialisieren und dann alle relevanten Daten vom alten Objekt auf das neue kopieren.
Abhängig von der Klasse ist es möglicherweise ein Container mit sehr vielen Daten, der viel Zeit und Speicherplatz beanspruchen kann
Mit move Semantik ist es nun möglich , die meisten dieser Arbeit weniger unangenehm , indem Sie einfach zu machen Bewegen der Daten anstatt zu kopieren.
Beim Verschieben der Daten werden die Daten erneut dem neuen Objekt zugeordnet. Und es findet überhaupt keine Kopie statt .
Dies wird mit einer
rvalue
Referenz erreicht.Eine
rvalue
Referenz funktioniert ziemlich ähnlich wie einelvalue
Referenz mit einem wichtigen Unterschied:Eine r-Wert-Referenz kann verschoben werden und eine l- Wert-Referenz nicht.
Von cppreference.com :
quelle
Ich schreibe dies, um sicherzustellen, dass ich es richtig verstehe.
Die Verschiebungssemantik wurde erstellt, um das unnötige Kopieren großer Objekte zu vermeiden. Bjarne Stroustrup verwendet in seinem Buch "The C ++ Programming Language" zwei Beispiele, bei denen standardmäßig unnötiges Kopieren auftritt: eines, das Austauschen von zwei großen Objekten und zwei, das Zurückgeben eines großen Objekts von einer Methode.
Das Austauschen von zwei großen Objekten umfasst normalerweise das Kopieren des ersten Objekts in ein temporäres Objekt, das Kopieren des zweiten Objekts in das erste Objekt und das Kopieren des temporären Objekts in das zweite Objekt. Für einen eingebauten Typ ist dies sehr schnell, aber für große Objekte können diese drei Kopien viel Zeit in Anspruch nehmen. Eine "Verschiebungszuweisung" ermöglicht es dem Programmierer, das Standardkopierverhalten zu überschreiben und stattdessen Verweise auf die Objekte auszutauschen, was bedeutet, dass überhaupt nicht kopiert wird und der Auslagerungsvorgang viel schneller ist. Die Verschiebungszuweisung kann durch Aufrufen der Methode std :: move () aufgerufen werden.
Das Zurückgeben eines Objekts von einer Methode umfasst standardmäßig das Erstellen einer Kopie des lokalen Objekts und der zugehörigen Daten an einem Ort, auf den der Aufrufer zugreifen kann (da das lokale Objekt für den Aufrufer nicht zugänglich ist und nach Abschluss der Methode verschwindet). Wenn ein integrierter Typ zurückgegeben wird, ist dieser Vorgang sehr schnell. Wenn jedoch ein großes Objekt zurückgegeben wird, kann dies lange dauern. Der Verschiebungskonstruktor ermöglicht es dem Programmierer, dieses Standardverhalten zu überschreiben und stattdessen die dem lokalen Objekt zugeordneten Heap-Daten "wiederzuverwenden", indem das an den Aufrufer zurückgegebene Objekt auf die dem lokalen Objekt zugeordneten Heap-Daten verweist. Somit ist kein Kopieren erforderlich.
In Sprachen, in denen keine lokalen Objekte erstellt werden können (d. H. Objekte auf dem Stapel), treten diese Arten von Problemen nicht auf, da alle Objekte auf dem Heap zugeordnet sind und immer als Referenz zugegriffen wird.
quelle
x
undy
können Sie nicht nur „swap Verweise auf die Objekte“ ; Es kann sein, dass die Objekte Zeiger enthalten, die auf andere Daten verweisen, und diese Zeiger können ausgetauscht werden, aber Verschiebungsoperatoren müssen nichts austauschen. Sie können die Daten aus dem verschobenen Objekt löschen, anstatt die darin enthaltenen Zieldaten beizubehalten.swap()
ohne Bewegungssemantik schreiben . "Die Verschiebungszuweisung kann durch Aufrufen der Methode std :: move () aufgerufen werden." - Es ist manchmal notwendig zu verwendenstd::move()
- obwohl das eigentlich nichts verschiebt - lässt den Compiler nur wissen, dass das Argument beweglich ist, manchmalstd::forward<>()
(mit Weiterleitungsreferenzen), und manchmal weiß der Compiler, dass ein Wert verschoben werden kann.Hier ist eine Antwort aus dem Buch "The C ++ Programming Language" von Bjarne Stroustrup. Wenn Sie das Video nicht sehen möchten, sehen Sie den folgenden Text:
Betrachten Sie diesen Ausschnitt. Bei der Rückkehr von einem Operator + wird das Ergebnis aus der lokalen Variablen
res
an einen Ort kopiert, an dem der Aufrufer darauf zugreifen kann.Wir wollten eigentlich keine Kopie; Wir wollten nur das Ergebnis aus einer Funktion herausholen. Wir müssen also einen Vektor verschieben, anstatt ihn zu kopieren. Wir können den Verschiebungskonstruktor wie folgt definieren:
Das && bedeutet "rWertreferenz" und ist eine Referenz, an die wir einen rWert binden können. "rvalue" 'soll "lvalue" ergänzen, was ungefähr "etwas bedeutet, das auf der linken Seite einer Aufgabe erscheinen kann". Ein r-Wert bedeutet also ungefähr "einen Wert, dem Sie nicht zuweisen können", z. B. eine von einem Funktionsaufruf zurückgegebene Ganzzahl und die
res
lokale Variable in Operator + () für Vektoren.Jetzt wird die Anweisung
return res;
nicht kopiert!quelle