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_ptrwird eine veraltete C ++ 98-Standardbibliotheksvorlage erläutert , die std::unique_ptrin 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_ptrist 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 bmit asich nicht auf das Dreieck kopieren, sondern überträgt das Eigentum des Dreiecks aus azu b. Wir sagen auch " awird 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_ptrsieht 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_ptrist, dass das, was syntaktisch wie eine Kopie aussieht, tatsächlich ein Zug ist. Wenn Sie versuchen, eine Mitgliedsfunktion für ein Verschieben von auto_ptraufzurufen, wird ein undefiniertes Verhalten ausgelöst. Sie müssen daher sehr vorsichtig sein, um keine zu verwenden, auto_ptrnachdem 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_ptraber 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 aund 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, ader eine auto_ptrVariable bezeichnet, und dem Ausdruck geben, make_triangle()der den Aufruf einer Funktion bezeichnet, die einen auto_ptrby-Wert zurückgibt , wodurch bei auto_ptrjedem Aufruf ein neues temporäres Objekt erstellt wird. aist 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 aist gefährlich, da wir später versuchen könnten, eine Mitgliedsfunktion über aaufzurufen 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 lund reinen 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 constin die Mischung werfen , haben wir bereits vier verschiedene Arten von Referenzen. An welche Arten von Typausdrücken Xkö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 Ynach erfolgt X. In diesem Fall wird ein temporärer Typ Xerstellt, 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::stringerstellt und ran 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_ptrKopierkonstruktor 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 aes sich um einen l- unique_ptr&& sourceWert 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 sourceist 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=, sourceverlä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::moveim Header aufgerufen wird <utility>. Dieser Name ist etwas unglücklich, weil er std::moveeinfach 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_rvalueoder 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 akein 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, aum zu initialisieren c; es interessiert mich nicht amehr. 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 returnAnweisung 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 resultals Argument akzeptiert ? Der Umfang von resultist kurz vor dem Ende und wird beim Abwickeln des Stapels zerstört. Niemand konnte sich danach beschweren, dass resultsich das irgendwie geändert hatte; Wenn der Kontrollfluss wieder beim Anrufer ist, resultexistiert 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::movediese 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::moveund 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 parameterein 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 parameterist nur eine gewöhnliche Variable mit einem Namen. Sie können parameterim 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 parameterdas nach der Initialisierung von nicht mehr verwendet wird member. Warum gibt es keine spezielle Regel zum stillen Einfügen std::movewie 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 returnSchlüsselwort ein automatisches Objekt bezeichnet oder nicht .
Sie können den parameterby-Wert auch übergeben. Für Nur-Bewegung-Typen wie unique_ptrscheint 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, Twird 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 TFall 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 Tund 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::moveimplementiert:
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, moveakzeptiert dank der Weiterleitungsreferenz jede Art von Parameter T&&und gibt eine rWert-Referenz zurück. Der std::remove_reference<T>::typeMetafunktionsaufruf ist notwendig, da andernfalls für lWerte vom Typ Xder Rückgabetyp wäre X& &&, der zusammenbrechen würde X&. Da tes sich immer um einen l-Wert handelt (denken Sie daran, dass eine benannte r-Wert-Referenz ein l-Wert ist), wir aber tan eine r-Wert-Referenz binden möchten, müssen wir explizit tin 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::moveein x-Wert.
Beachten Sie, dass die Rückgabe per rvalue-Referenz in diesem Beispiel in Ordnung ist, da tdies kein automatisches Objekt bezeichnet, sondern ein Objekt, das vom Aufrufer übergeben wurde.