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:
class cannot_benefit_from_move_semantics
{
int a; // moving an int means copying an int
float b; // moving a float means copying a float
double c; // moving a double means copying a double
char d[64]; // moving a char array means copying a char array
// ...
};
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 , die std::unique_ptr
in C ++ 11 durch ersetzt wurde. Fortgeschrittene C ++ - Programmierer sind wahrscheinlich zumindest einigermaßen vertraut std::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 sind auto_ptr
, soll damit sichergestellt werden, dass ein dynamisch zugewiesenes Objekt auch bei Ausnahmen immer freigegeben wird:
{
std::auto_ptr<Shape> a(new Triangle);
// ...
// arbitrary code, could throw exceptions
// ...
} // <--- when a goes out of scope, the triangle is deleted automatically
Das Ungewöhnliche auto_ptr
ist das "Kopieren":
auto_ptr<Shape> a(new Triangle);
+---------------+
| triangle data |
+---------------+
^
|
|
|
+-----|---+
| +-|-+ |
a | p | | | |
| +---+ |
+---------+
auto_ptr<Shape> b(a);
+---------------+
| triangle data |
+---------------+
^
|
+----------------------+
|
+---------+ +-----|---+
| +---+ | | +-|-+ |
a | p | | | b | p | | | |
| +---+ | | +---+ |
+---------+ +---------+
Beachten Sie, wie die Initialisierung b
mit a
sich nicht auf das Dreieck kopieren, sondern überträgt das Eigentum des Dreiecks aus a
zu b
. Wir sagen auch " a
wird verschoben in b
" oder "das Dreieck wird verschoben von a
nach b
". Dies mag verwirrend klingen, da das Dreieck selbst immer an derselben Stelle im Speicher bleibt.
Ein Objekt zu verschieben bedeutet, den Besitz einer von ihm verwalteten Ressource auf ein anderes Objekt zu übertragen.
Der Kopierkonstruktor von auto_ptr
sieht wahrscheinlich ungefähr so aus (etwas vereinfacht):
auto_ptr(auto_ptr& source) // note the missing const
{
p = source.p;
source.p = 0; // now the source no longer owns the object
}
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 von auto_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:
auto_ptr<Shape> a(new Triangle); // create triangle
auto_ptr<Shape> b(a); // move a into b
double area = a->area(); // undefined behavior
Ist auto_ptr
aber nicht immer gefährlich. Werksfunktionen sind ein perfekter Anwendungsfall für auto_ptr
:
auto_ptr<Shape> make_triangle()
{
return auto_ptr<Shape>(new Triangle);
}
auto_ptr<Shape> c(make_triangle()); // move temporary into c
double area = make_triangle()->area(); // perfectly safe
Beachten Sie, wie beide Beispiele demselben syntaktischen Muster folgen:
auto_ptr<Shape> variable(expression);
double area = expression->area();
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
und make_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 eine auto_ptr
Variable bezeichnet, und dem Ausdruck geben, make_triangle()
der den Aufruf einer Funktion bezeichnet, die einen auto_ptr
by-Wert zurückgibt , wodurch bei auto_ptr
jedem Aufruf ein neues temporäres Objekt erstellt wird. a
ist ein Beispiel für einen l-Wert , während make_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 über a
aufzurufen und undefiniertes Verhalten aufzurufen. Auf der anderen Seite ist es absolut make_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 noch make_triangle()
einmal schreiben , erhalten wir eine andere temporäre. Tatsächlich ist das vorübergehende Verschieben bereits in der nächsten Zeile verschwunden:
auto_ptr<Shape> c(make_triangle());
^ the moved-from temporary dies right here
Beachten Sie, dass die Buchstaben l
und r
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).
Ein rWert vom Klassentyp ist ein Ausdruck, dessen Auswertung ein temporäres Objekt erstellt. Unter normalen Umständen bezeichnet kein anderer Ausdruck innerhalb desselben Bereichs dasselbe temporäre Objekt.
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 Referenz X&
ist jetzt als Wertreferenz bekannt . (Beachten Sie, dass X&&
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ücken X
können sie binden?
lvalue const lvalue rvalue const rvalue
---------------------------------------------------------
X& yes
const X& yes yes yes yes
X&& yes
const X&& yes yes
In der Praxis können Sie vergessen const X&&
. Es ist nicht sehr nützlich, sich auf das Lesen von Werten zu beschränken.
Eine r-Wert-Referenz X&&
ist eine neue Art von Referenz, die nur an r-Werte bindet.
Implizite Konvertierungen
R-Wert-Referenzen durchliefen mehrere Versionen. Seit Version 2.1 X&&
bindet eine rvalue-Referenz auch an alle Wertekategorien eines anderen Typs Y
, sofern eine implizite Konvertierung von Y
nach erfolgt X
. In diesem Fall wird ein temporärer Typ X
erstellt, und die r-Wert-Referenz ist an diesen temporären Typ gebunden:
void some_function(std::string&& r);
some_function("hello world");
Im obigen Beispiel "hello world"
ist ein Wert vom Typ const char[12]
. Da es eine implizite Konvertierung von const char[12]
bis const char*
nach gibt std::string
, wird ein temporäres vom Typ std::string
erstellt und r
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 Verschiebungskonstruktor X::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 diskutieren unique_ptr
. Erstens kapseln wir einen Rohzeiger und überlasten die Betreiber ->
und *
, so dass unsere Klasse fühlt sich wie ein Zeiger:
template<typename T>
class unique_ptr
{
T* ptr;
public:
T* operator->() const
{
return ptr;
}
T& operator*() const
{
return *ptr;
}
Der Konstruktor übernimmt das Eigentum an dem Objekt und der Destruktor löscht es:
explicit unique_ptr(T* p = nullptr)
{
ptr = p;
}
~unique_ptr()
{
delete ptr;
}
Nun kommt der interessante Teil, der Verschiebungskonstruktor:
unique_ptr(unique_ptr&& source) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
}
Dieser Verschiebungskonstruktor macht genau das, was der auto_ptr
Kopierkonstruktor getan hat, kann jedoch nur mit rWerten geliefert werden:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // error
unique_ptr<Shape> c(make_triangle()); // okay
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, da make_triangle()
es sich um einen Wert handelt. Der Verschiebungskonstruktor überträgt das Eigentum von der temporären auf c
. Auch dies ist genau das, was wir wollten.
Der Verschiebungskonstruktor überträgt das Eigentum an einer verwalteten Ressource auf das aktuelle Objekt.
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:
unique_ptr& operator=(unique_ptr&& source) // note the rvalue reference
{
if (this != &source) // beware of self-assignment
{
delete ptr; // release the old resource
ptr = source.ptr; // acquire the new resource
source.ptr = nullptr;
}
return *this;
}
};
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:
unique_ptr& operator=(unique_ptr source) // note the missing reference
{
std::swap(ptr, source.ptr);
return *this;
}
};
Dies source
ist eine Variable vom Typ unique_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 erreicht operator=
, source
verlässt er den Gültigkeitsbereich und gibt die alte Ressource automatisch frei.
Der Verschiebungszuweisungsoperator überträgt das Eigentum an einer verwalteten Ressource auf das aktuelle Objekt und gibt die alte Ressource frei. Die Move-and-Swap-Sprache vereinfacht die Implementierung.
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 er std::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 sollen std::cast_to_rvalue
oder std::enable_move
, aber wir bleiben jetzt beim Namen.
So bewegen Sie sich explizit von einem Wert:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // still an error
unique_ptr<Shape> c(std::move(a)); // okay
Beachten Sie, dass nach der dritten Zeile a
kein Dreieck mehr vorhanden ist. Das ist in Ordnung, denn durch explizites Schreiben std::move(a)
haben wir unsere Absichten klargestellt: "Lieber Konstruktor, machen Sie, was Sie wollen, a
um zu initialisieren c
; es interessiert mich nicht a
mehr. Fühlen Sie sich frei, Ihren Weg mit zu haben a
."
std::move(some_lvalue)
Wirft einen l-Wert in einen r-Wert und ermöglicht so einen nachfolgenden Zug.
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:
expressions
/ \
/ \
/ \
glvalues rvalues
/ \ / \
/ \ / \
/ \ / \
lvalues xvalues prvalues
Beachten Sie, dass nur x-Werte wirklich neu sind. Der Rest ist nur auf das Umbenennen und Gruppieren zurückzuführen.
C ++ 98-Werte werden in C ++ 11 als Werte bezeichnet. Ersetzen Sie geistig alle Vorkommen von "rvalue" in den vorhergehenden Absätzen durch "prvalue".
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 :
unique_ptr<Shape> make_triangle()
{
return unique_ptr<Shape>(new Triangle);
} \-----------------------------/
|
| temporary is moved into c
|
v
unique_ptr<Shape> c(make_triangle());
Es ist vielleicht überraschend, dass automatische Objekte (lokale Variablen, die nicht als deklariert sind static
) auch implizit aus Funktionen verschoben werden können:
unique_ptr<Shape> make_square()
{
unique_ptr<Shape> result(new Square);
return result; // note the missing std::move
}
Wie kommt es, dass der Verschiebungskonstruktor den l-Wert result
als Argument akzeptiert ? Der Umfang von result
ist kurz vor dem Ende und wird beim Abwickeln des Stapels zerstört. Niemand konnte sich danach beschweren, dass result
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üssen std::move
. Tatsächlich sollten Sie niemals verwenden std::move
, um automatische Objekte aus Funktionen zu verschieben, da dies die "Named Return Value Optimization" (NRVO) verhindert.
Verwenden Sie std::move
diese Option niemals , um automatische Objekte aus Funktionen zu entfernen.
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:
unique_ptr<Shape>&& flawed_attempt() // DO NOT DO THIS!
{
unique_ptr<Shape> very_bad_idea(new Square);
return std::move(very_bad_idea); // WRONG!
}
Geben Sie niemals automatische Objekte als Wertreferenz zurück. Das Verschieben wird ausschließlich vom Verschiebungskonstruktor ausgeführt, nicht von std::move
und nicht nur durch Binden eines r-Werts an eine r-Wert-Referenz.
Umzug in Mitglieder
Früher oder später werden Sie Code wie folgt schreiben:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(parameter) // error
{}
};
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 Tat parameter
ist nur eine gewöhnliche Variable mit einem Namen. Sie können parameter
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.
Eine benannte rvalue-Referenz ist wie jede andere Variable ein lvalue.
Die Lösung besteht darin, den Umzug manuell zu aktivieren:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(std::move(parameter)) // note the std::move
{}
};
Man könnte argumentieren, dass parameter
das nach der Initialisierung von nicht mehr verwendet wird member
. Warum gibt es keine spezielle Regel zum stillen Einfügen std::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 dem return
Schlüsselwort ein automatisches Objekt bezeichnet oder nicht .
Sie können den parameter
by-Wert auch übergeben. Für Nur-Bewegung-Typen wie unique_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.
X::X(const X&); // copy constructor
X& X::operator=(const X&); // copy assignment operator
X::~X(); // destructor
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.
X::X(X&&); // move constructor
X& X::operator=(X&&); // move assignment operator
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?
Wenn Sie eine Klasse ohne nicht verwaltete Ressourcen schreiben, müssen Sie keine der fünf speziellen Elementfunktionen selbst deklarieren, und Sie erhalten kostenlos die richtige Kopiersemantik und Verschiebungssemantik. Andernfalls müssen Sie die speziellen Elementfunktionen selbst implementieren. Wenn Ihre Klasse nicht von der Verschiebungssemantik profitiert, müssen die speziellen Verschiebungsoperationen natürlich nicht implementiert werden.
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:
X& X::operator=(X source) // unified assignment operator
{
swap(source); // see my first answer for an explanation
return *this;
}
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:
template<typename T>
void foo(T&&);
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:
foo(make_triangle()); // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a); // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&
Wenn das Argument ein R - Wert vom Typ ist X
, T
wird abgeleitet zu sein X
, daher T&&
bedeutet X&&
. Das würde jeder erwarten. Wenn es sich bei dem Argument jedoch um einen Wert vom Typ handelt X
, wird aufgrund einer Sonderregel davon ausgegangen, dass dies der T
Fall ist X&
, und T&&
würde daher so etwas wie bedeuten X& &&
. Aber da C ++ noch keine Ahnung von Verweisen auf Referenzen hat, die Art X& &&
ist , kollabiert in X&
. 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).
T && ist keine Wertreferenz, sondern eine Weiterleitungsreferenz. Es bindet auch an lWerte, in diesem Fall T
und T&&
sind beide lWertreferenzen.
Wenn Sie eine Funktionsvorlage auf rWerte beschränken möchten, können Sie SFINAE mit Typmerkmalen kombinieren :
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);
Umsetzung des Umzugs
Nachdem Sie nun das Reduzieren von Referenzen verstanden haben, wird Folgendes std::move
implementiert:
template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
Wie Sie sehen können, move
akzeptiert dank der Weiterleitungsreferenz jede Art von Parameter T&&
und gibt eine rWert-Referenz zurück. Der std::remove_reference<T>::type
Metafunktionsaufruf ist notwendig, da andernfalls für lWerte vom Typ X
der Rückgabetyp wäre X& &&
, der zusammenbrechen würde X&
. Da t
es sich immer um einen l-Wert handelt (denken Sie daran, dass eine benannte r-Wert-Referenz ein l-Wert ist), wir aber t
an eine r-Wert-Referenz binden möchten, müssen wir explizit t
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;)
Der Aufruf einer Funktion, die eine r-Wert-Referenz zurückgibt, z. B. std::move
ein x-Wert.
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.