Meine erste Antwort war eine extrem vereinfachte Einführung in die Move-Semantik, und viele Details wurden absichtlich weggelassen, um sie einfach zu halten.Allerdings gibt es noch viel mehr zur Move-Semantik, 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, sie eignet sich immer noch gut für eine erste Einführung. Aber wenn Sie tiefer einsteigen wollen, lesen Sie weiter 🙂
Stephan T. Lavavej hat sich die Zeit genommen, wertvolles Feedback zu geben. Vielen Dank, Stephan!
- Einführung
- Was ist ein Move?
- Gefährliche und harmlose Verschiebungen
- Wertkategorien
- RWert-Referenzen
- Implizite Konvertierungen
- Move-Konstruktoren
- Zuweisungsoperatoren verschieben
- Moving from lvalues
- XWerte
- Aus Funktionen herausbewegen
- Moving into members
- Spezielle Mitgliedsfunktionen
- Weiterleitende Referenzen (früher bekannt als Universalreferenzen)
- Implementierung von move
Einführung
Die Bewegungssemantik erlaubt es einem Objekt, unter bestimmten Bedingungen das Eigentum an den externen Ressourcen eines anderen Objekts zu übernehmen. Das ist in zweierlei Hinsicht wichtig:
-
Das macht teure Kopien zu billigen Verschiebungen. Siehe meine erste Antwort für ein Beispiel. Beachten Sie, dass, wenn ein Objekt nicht mindestens eine externe Ressource verwaltet (entweder direkt oder indirekt über seine Mitgliedsobjekte), die Verschiebesemantik keine Vorteile gegenüber der Kopiersemantik bietet. In diesem Fall bedeutet das Kopieren eines Objekts und das Verschieben eines Objekts genau das Gleiche:
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; // moving a char array means copying a char array // ...};
-
Implementieren von sicheren „move-only“-Typen; das heißt, Typen, für die das Kopieren keinen Sinn macht, das Verschieben aber schon. Beispiele sind Sperren, Datei-Handles und intelligente Zeiger mit eindeutiger Besitz-Semantik. Hinweis: Diese Antwort behandelt
std::auto_ptr
, ein veraltetes C++98-Standardbibliotheks-Template, das in C++11 durchstd::unique_ptr
ersetzt wurde. Fortgeschrittene C++-Programmierer sind wahrscheinlich zumindest einigermaßen mitstd::auto_ptr
vertraut, und wegen der „Verschiebesemantik“, die es anzeigt, scheint es ein guter Ausgangspunkt für die Diskussion der Verschiebesemantik in C++11 zu sein. YMMV.
Was ist ein Move?
Die C++98-Standardbibliothek bietet einen intelligenten Zeiger mit einzigartiger Eigentumssemantik namens std::auto_ptr<T>
. Falls Sie mit auto_ptr
nicht vertraut sind, sein Zweck ist es, zu garantieren, dass ein dynamisch zugewiesenes Objekt immer freigegeben wird, sogar angesichts von Ausnahmen:
{ 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 an auto_ptr
ist sein „Kopier“-Verhalten:
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 von b
mit a
das Dreieck nicht kopiert, sondern stattdessen das Eigentum an dem Dreieck von a
auf b
überträgt. Wir sagen auch „a
wird in b
verschoben“ oder „das Dreieck wird von a
nach b
verschoben“. Das mag verwirrend klingen, weil das Dreieck selbst immer an der gleichen Stelle im Speicher bleibt.
Ein Objekt zu verschieben bedeutet, das Eigentum an einer Ressource, die es verwaltet, auf ein anderes Objekt zu übertragen.
Der Kopierkonstruktor von auto_ptr
sieht wahrscheinlich etwa 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 Verschiebungen
Das Gefährliche an auto_ptr
ist, dass das, was syntaktisch wie eine Kopie aussieht, in Wirklichkeit eine Verschiebung ist. Der Versuch, eine Mitgliedsfunktion auf einem verschobenen auto_ptr
aufzurufen, führt zu undefiniertem Verhalten, also muss man sehr vorsichtig sein, einen auto_ptr
nicht zu benutzen, nachdem er verschoben wurde:
auto_ptr<Shape> a(new Triangle); // create triangleauto_ptr<Shape> b(a); // move a into bdouble area = a->area(); // undefined behavior
Aber auto_ptr
ist nicht immer gefährlich. Fabrikfunktionen sind ein sehr guter 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 cdouble area = make_triangle()->area(); // perfectly safe
Beachten Sie, dass beide Beispiele demselben syntaktischen Muster folgen:
auto_ptr<Shape> variable(expression);double area = expression->area();
Und dennoch ruft eines von ihnen ein undefiniertes Verhalten auf, während das andere dies nicht tut. Was ist also der Unterschied zwischen den Ausdrücken a
und make_triangle()
? Sind sie nicht beide vom gleichen Typ? Das sind sie in der Tat, aber sie haben unterschiedliche Wertkategorien.
Wertkategorien
Natürlich muss es einen tiefgreifenden Unterschied geben zwischen dem Ausdruck a
, der eine auto_ptr
-Variable bezeichnet, und dem Ausdruck make_triangle()
, der den Aufruf einer Funktion bezeichnet, die eine auto_ptr
als Wert zurückgibt und somit bei jedem Aufruf ein neues temporäres auto_ptr
-Objekt erzeugt. a
ist ein Beispiel für einen l-Wert, während make_triangle()
ein Beispiel für einen r-Wert ist.
Der Wechsel von l-Werten wie a
ist gefährlich, weil wir später versuchen könnten, eine Mitgliedsfunktion über a
aufzurufen, was ein undefiniertes Verhalten hervorrufen würde. Andererseits ist das Verschieben von rWerten wie make_triangle()
vollkommen sicher, denn nachdem der Kopierkonstruktor seine Arbeit getan hat, können wir den temporären Wert nicht mehr verwenden. Es gibt keinen Ausdruck, der dieses Provisorium bezeichnet; wenn wir einfach make_triangle()
erneut schreiben, erhalten wir ein anderes Provisorium. Tatsächlich ist das verschobene Provisorium 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 in der linken und rechten Seite einer Zuweisung haben. Dies ist in C++ nicht mehr der Fall, denn es gibt lWerte, die nicht auf der linken Seite einer Zuweisung erscheinen 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 von Klassentyp ist ein Ausdruck, dessen Auswertung ein temporäres Objekt erzeugt. Unter normalen Umständen bezeichnet kein anderer Ausdruck innerhalb desselben Geltungsbereichs dasselbe temporäre Objekt.
RWert-Referenzen
Wir verstehen jetzt, dass der Wechsel von lWerten potentiell gefährlich ist, aber der Wechsel von rWerten ist harmlos. Wenn C++ eine Sprachunterstützung hätte, um lwertige Argumente von rwertigen Argumenten zu unterscheiden, könnten wir entweder das Verschieben von lwerten vollständig verbieten oder zumindest das Verschieben von lwerten am Aufrufort explizit machen, so dass wir nicht mehr aus Versehen verschieben.
C++11s Antwort auf dieses Problem sind rwertige Referenzen. Eine rvalue-Referenz ist eine neue Art von Referenz, die sich nur an rvalues bindet, und die Syntax ist X&&
. Die gute alte Referenz X&
ist jetzt als lvalue-Referenz bekannt. (Beachten Sie, dass X&&
keine Referenz auf eine Referenz ist; so etwas gibt es in C++ nicht.)
Wenn wir const
in den Mix werfen, haben wir bereits vier verschiedene Arten von Referenzen. An welche Arten von Ausdrücken vom Typ X
können sie sich binden?
lvalue const lvalue rvalue const rvalue--------------------------------------------------------- X& yesconst X& yes yes yes yesX&& yesconst X&& yes yes
In der Praxis kann man const X&&
vergessen. Die Beschränkung auf das Lesen von rWerten ist nicht sehr nützlich.
Eine rWert-Referenz
X&&
ist eine neue Art von Referenz, die nur an rWerte bindet.
Implizite Konvertierungen
Wert-Referenzen haben mehrere Versionen durchlaufen. Seit Version 2.1 bindet eine rvalue-Referenz X&&
auch an alle Wertkategorien eines anderen Typs Y
, sofern es eine implizite Konvertierung von Y
nach X
gibt. In diesem Fall wird ein Provisorium des Typs X
erstellt, und die rWert-Referenz ist an dieses Provisorium gebunden:
void some_function(std::string&& r);some_function("hello world");
Im obigen Beispiel ist "hello world"
ein lWert des Typs const char
. Da es eine implizite Konvertierung von const char
über const char*
nach std::string
gibt, wird ein temporärer Wert des Typs std::string
erstellt, und r
wird an diesen temporären Wert gebunden. Dies ist einer der Fälle, in denen die Unterscheidung zwischen rValues (Ausdrücken) und Temporären (Objekten) etwas unscharf ist.
Move-Konstruktoren
Ein nützliches Beispiel für eine Funktion mit einem X&&
-Parameter ist der Move-Konstruktor X::X(X&& source)
. Sein Zweck ist es, das Eigentum an der verwalteten Ressource von der Quelle auf das aktuelle Objekt zu übertragen.
In C++11 wurde std::auto_ptr<T>
durch std::unique_ptr<T>
ersetzt, das die Vorteile von rvalue-Referenzen nutzt. Ich werde eine vereinfachte Version von unique_ptr
entwickeln und diskutieren. Zuerst kapseln wir einen rohen Zeiger und überladen die Operatoren ->
und *
, so dass sich unsere Klasse wie ein Zeiger anfühlt:
template<typename T>class unique_ptr{ T* ptr;public: T* operator->() const { return ptr; } T& operator*() const { return *ptr; }
Der Konstruktor übernimmt das Objekt, und der Destruktor löscht es:
explicit unique_ptr(T* p = nullptr) { ptr = p; } ~unique_ptr() { delete ptr; }
Jetzt kommt der interessante Teil, der Move-Konstruktor:
unique_ptr(unique_ptr&& source) // note the rvalue reference { ptr = source.ptr; source.ptr = nullptr; }
Dieser move-Konstruktor macht genau das, was der auto_ptr
copy-Konstruktor gemacht hat, aber er kann nur mit rWerten versorgt werden:
unique_ptr<Shape> a(new Triangle);unique_ptr<Shape> b(a); // errorunique_ptr<Shape> c(make_triangle()); // okay
Die zweite Zeile lässt sich nicht kompilieren, weil a
ein lWert ist, aber der Parameter unique_ptr&& source
kann nur an rWerte gebunden werden. Das ist genau das, was wir wollten; gefährliche Bewegungen sollten niemals implizit sein. Die dritte Zeile lässt sich problemlos kompilieren, da make_triangle()
ein r-Wert ist. Der move-Konstruktor überträgt den Besitz vom temporären Wert auf c
. Auch dies ist genau das, was wir wollten.
Der move-Konstruktor überträgt das Eigentum an einer verwalteten Ressource auf das aktuelle Objekt.
Zuweisungsoperatoren verschieben
Das letzte fehlende Teil ist der Zuweisungsoperator verschieben. Seine Aufgabe ist es, die alte Ressource freizugeben und die neue Ressource aus seinem Argument zu übernehmen:
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 Zuweisungsoperators move die Logik sowohl des Destruktors als auch des Konstruktors move dupliziert. Sind Sie mit dem Copy-and-Swap-Idiom vertraut? Es kann auch auf die move-Semantik als move-and-swap-Idiom angewendet werden:
unique_ptr& operator=(unique_ptr source) // note the missing reference { std::swap(ptr, source.ptr); return *this; }};
Nun, da source
eine Variable des Typs unique_ptr
ist, wird sie vom move-Konstruktor initialisiert; das heißt, das Argument wird in den Parameter verschoben. Das Argument muss immer noch ein r-Wert sein, da der move-Konstruktor selbst einen r-Wert-Referenzparameter hat. Wenn der Kontrollfluss die schließende Klammer von operator=
erreicht, geht source
aus dem Geltungsbereich heraus und gibt die alte Ressource automatisch frei.
Der Zuweisungsoperator move überträgt das Eigentum an einer verwalteten Ressource auf das aktuelle Objekt und gibt die alte Ressource frei. Das move-and-swap Idiom vereinfacht die Implementierung.
Moving from lvalues
Manchmal wollen wir von lvalues aus verschieben. Das heißt, manchmal wollen wir, dass der Compiler einen lWert so behandelt, als wäre er ein rWert, so dass er den move-Konstruktor aufrufen kann, obwohl er potenziell unsicher sein könnte.Für diesen Zweck bietet C++11 eine Standardbibliotheksfunktionsschablone namens std::move
im Header <utility>
.Dieser Name ist etwas unglücklich, denn std::move
castet einfach einen lWert in einen rWert; es bewegt selbst nichts. Er ermöglicht lediglich das Verschieben. Vielleicht hätte es std::cast_to_rvalue
oder std::enable_move
heißen sollen, aber wir sind jetzt bei dem Namen hängengeblieben.
So verschiebt man explizit von einem l-Wert:
unique_ptr<Shape> a(new Triangle);unique_ptr<Shape> b(a); // still an errorunique_ptr<Shape> c(std::move(a)); // okay
Nach der dritten Zeile besitzt a
kein Dreieck mehr. Das ist in Ordnung, denn durch das explizite Schreiben von std::move(a)
haben wir unsere Absichten deutlich gemacht: „Lieber Konstruktor, mach mit a
, was immer du willst, um c
zu initialisieren; a
interessiert mich nicht mehr. Du kannst mit a
machen, was du willst.“
std::move(some_lvalue)
wandelt einen l-Wert in einen r-Wert um und ermöglicht so einen anschließenden Umzug.
XWerte
Beachte, dass std::move(a)
zwar ein r-Wert ist, seine Auswertung aber kein temporäres Objekt erzeugt. Dieses Rätsel zwang den Ausschuss, eine dritte Wertkategorie einzuführen. Etwas, das an eine rvalue-Referenz gebunden werden kann, obwohl es kein rvalue im traditionellen Sinne ist, wird als xvalue (eXpiring value) bezeichnet. Die traditionellen rvalues wurden in prvalues (Pure rvalues) umbenannt.
Bei prvalues und xvalues handelt es sich um rvalues. XWerte und lWerte sind beide glWerte (Generalized lWerte). Die Beziehungen sind mit einem Diagramm leichter zu erfassen:
expressions / \ / \ / \ glvalues rvalues / \ / \ / \ / \ / \ / \lvalues xvalues prvalues
Beachten Sie, dass nur xvalues wirklich neu sind; der Rest ist nur auf die Umbenennung und Gruppierung zurückzuführen.
C++98 rvalues sind in C++11 als prvalues bekannt. Ersetzen Sie gedanklich alle Vorkommen von „rvalue“ in den vorhergehenden Absätzen durch „prvalue“.
Aus Funktionen herausbewegen
Bis jetzt haben wir die Bewegung in lokale Variablen und in Funktionsparameter gesehen. Aber auch in der umgekehrten Richtung ist eine Bewegung möglich. Wenn eine Funktion mit einem Wert zurückkehrt, wird ein Objekt am Aufrufort (wahrscheinlich eine lokale Variable oder ein temporäres Objekt, aber es kann jede Art von Objekt sein) mit dem Ausdruck nach der return
-Anweisung als Argument für den move-Konstruktor initialisiert:
unique_ptr<Shape> make_triangle(){ return unique_ptr<Shape>(new Triangle);} \-----------------------------/ | | temporary is moved into c | vunique_ptr<Shape> c(make_triangle());
Vielleicht überraschenderweise können auch automatische Objekte (lokale Variablen, die nicht als static
deklariert sind) implizit aus Funktionen verschoben werden:
unique_ptr<Shape> make_square(){ unique_ptr<Shape> result(new Square); return result; // note the missing std::move}
Warum akzeptiert der move-Konstruktor den l-Wert result
als Argument? Der Gültigkeitsbereich von result
wird bald enden, und er wird beim Abwickeln des Stapels zerstört werden. Niemand könnte sich hinterher beschweren, dass sich result
irgendwie verändert hat; wenn der Kontrollfluss wieder beim Aufrufer ist, existiert result
nicht mehr! Aus diesem Grund gibt es in C++11 eine spezielle Regel, die es erlaubt, automatische Objekte aus Funktionen zurückzugeben, ohne std::move
schreiben zu müssen. Tatsächlich sollten Sie niemals std::move
verwenden, um automatische Objekte aus Funktionen zu verschieben, da dies die „named return value optimization“ (NRVO) verhindert.
Verwenden Sie niemals
std::move
, um automatische Objekte aus Funktionen zu verschieben.
Beachten Sie, dass in beiden Factory-Funktionen der Rückgabetyp ein Wert und keine rvalue-Referenz ist. Rvalue-Referenzen sind immer noch Referenzen, und wie immer sollten Sie niemals eine Referenz auf ein automatisches Objekt zurückgeben; der Aufrufer würde mit einer baumelnden Referenz enden, wenn Sie den Compiler austricksen, damit er Ihren Code akzeptiert, wie hier:
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 per rvalue-Referenz zurück. Das Verschieben wird ausschließlich durch den move-Konstruktor durchgeführt, nicht durch
std::move
, und auch nicht durch das bloße Binden eines rWertes an eine rWert-Referenz.
Moving into members
Früher oder später werden Sie Code wie diesen schreiben:
class Foo{ unique_ptr<Shape> member;public: Foo(unique_ptr<Shape>&& parameter) : member(parameter) // error {}};
Grundsätzlich wird sich der Compiler beschweren, dass parameter
ein lWert ist. Wenn Sie sich den Typ ansehen, sehen Sie eine rWert-Referenz, aber eine rWert-Referenz bedeutet einfach „eine Referenz, die an einen rWert gebunden ist“; sie bedeutet nicht, dass die Referenz selbst ein rWert ist! In der Tat ist parameter
nur eine gewöhnliche Variable mit einem Namen. Sie können parameter
beliebig oft innerhalb des Konstruktorkörpers verwenden, und es bezeichnet immer dasselbe Objekt. Eine implizite Verschiebung von ihr wäre gefährlich, daher verbietet die Sprache dies.
Eine benannte rvalue-Referenz ist ein lvalue, genau wie jede andere Variable.
Die Lösung ist, die Verschiebung 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
nach der Initialisierung von member
nicht mehr verwendet wird. Warum gibt es keine spezielle Regel, um std::move
stillschweigend einzufügen, genau wie bei Rückgabewerten? Wahrscheinlich, weil dies eine zu große Belastung für die Compiler-Implementierer wäre. Was wäre zum Beispiel, wenn der Konstruktorkörper in einer anderen Übersetzungseinheit läge? Im Gegensatz dazu muss die Rückgabewertregel lediglich die Symboltabellen überprüfen, um festzustellen, ob der Bezeichner nach dem Schlüsselwort return
ein automatisches Objekt bezeichnet oder nicht.
Sie können parameter
auch als Wert übergeben. Für „move-only“-Typen wie unique_ptr
scheint es noch kein etabliertes Idiom zu geben. Ich persönlich bevorzuge die Wertübergabe, da sie weniger Unordnung in der Schnittstelle verursacht.
Spezielle Mitgliedsfunktionen
C++98 deklariert implizit drei spezielle Mitgliedsfunktionen bei Bedarf, d.h. wenn sie irgendwo benötigt werden: den Kopierkonstruktor, den Kopierzuweisungsoperator und den Destruktor.
X::X(const X&); // copy constructorX& X::operator=(const X&); // copy assignment operatorX::~X(); // destructor
R-Wert-Referenzen haben mehrere Versionen durchlaufen. Seit Version 3.0 deklariert C++11 zwei zusätzliche spezielle Memberfunktionen auf Anfrage: den move-Konstruktor und den move-Zuweisungsoperator. Beachten Sie, dass weder VC10 noch VC11 mit der Version 3.0 konform sind, so dass Sie diese Funktionen selbst implementieren müssen.
X::X(X&&); // move constructorX& X::operator=(X&&); // move assignment operator
Diese beiden neuen speziellen Memberfunktionen werden nur dann implizit deklariert, wenn keine der speziellen Memberfunktionen manuell deklariert wird. Auch wenn Sie Ihren eigenen Move-Konstruktor oder Move-Zuweisungsoperator deklarieren, werden weder der Copy-Konstruktor noch der Copy-Zuweisungsoperator implizit deklariert.
Was bedeuten diese Regeln in der Praxis?
Wenn Sie eine Klasse ohne unverwaltete Ressourcen schreiben, müssen Sie keine der fünf speziellen Memberfunktionen selbst deklarieren, und Sie erhalten die korrekte Copy-Semantik und Move-Semantik umsonst. Andernfalls müssen Sie die speziellen Mitgliedsfunktionen selbst implementieren. Wenn Ihre Klasse nicht von der Move-Semantik profitiert, brauchen Sie die speziellen Move-Operationen natürlich nicht zu implementieren.
Beachten Sie, dass der Kopier-Zuweisungsoperator und der Move-Zuweisungsoperator zu einem einzigen, vereinheitlichten Zuweisungsoperator verschmolzen werden können, der sein Argument als Wert annimmt:
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 Mitgliedsfunktionen von fünf auf vier. Es gibt hier einen Kompromiss zwischen Ausnahmesicherheit und Effizienz, aber ich bin kein Experte in dieser Frage.
Weiterleitende Referenzen (früher bekannt als Universalreferenzen)
Betrachten Sie die folgende Funktionsvorlage:
template<typename T>void foo(T&&);
Man könnte erwarten, dass T&&
nur an rWerte bindet, weil es auf den ersten Blick wie eine rWert-Referenz aussieht. Es stellt sich jedoch heraus, dass T&&
auch an lWerte bindet:
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 rWert des Typs X
ist, wird T
als X
abgeleitet, also bedeutet T&&
X&&
. Wenn das Argument jedoch ein l-Wert vom Typ X
ist, wird aufgrund einer speziellen Regel abgeleitet, dass T
X&
ist, so dass T&&
etwas wie X& &&
bedeuten würde. Da C++ aber noch keinen Begriff von Referenzen auf Referenzen hat, wird der Typ X& &&
in X&
kollabiert. Das mag sich zunächst verwirrend und nutzlos anhören, aber das Kollabieren von Referenzen ist essentiell für eine perfekte Weiterleitung (auf die hier nicht eingegangen wird).
T&& ist keine rvalue-Referenz, sondern eine Weiterleitungsreferenz. Es bindet auch an lWerte, in diesem Fall sind
T
undT&&
beides lWert-Referenzen.
Wenn man eine Funktionsschablone auf rWerte beschränken will, kann man SFINAE mit Type Traits kombinieren:
#include <type_traits>template<typename T>typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::typefoo(T&&);
Implementierung von move
Nun, da man das Zusammenfallen von Referenzen verstanden hat, hier ist, wie std::move
implementiert wird:
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, akzeptiert move
dank der Weiterleitungsreferenz T&&
jede Art von Parameter und gibt eine rWert-Referenz zurück. Der Meta-Funktionsaufruf std::remove_reference<T>::type
ist notwendig, weil der Rückgabetyp für lWerte vom Typ X
sonst X& &&
wäre, was zu X&
führen würde. Da t
immer ein l-Wert ist (denken Sie daran, dass eine benannte r-Wert-Referenz ein l-Wert ist), wir aber t
an eine r-Wert-Referenz binden wollen, müssen wir t
explizit in den richtigen Rückgabetyp umwandeln Der Aufruf einer Funktion, die eine r-Wert-Referenz zurückgibt, ist selbst ein x-Wert. Jetzt wissen Sie, woher xvalues kommen 😉