Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
In diesem Thema werden die verschiedenen Kategorien von Werten (und Verweise auf Werte) vorgestellt und beschrieben, die in C++ vorhanden sind:
- glvalue
- Lvalue
- xlvalue
- prvalue
- Rvalue
Sie werden zweifellos von l-Werten und r-Werten gehört haben. Aber Sie denken vielleicht nicht an sie in den Begriffen, die in diesem Thema präsentiert werden.
Jeder Ausdruck in C++ liefert einen Wert, der zu einer der fünf oben aufgeführten Kategorien gehört. Es gibt Aspekte der C++-Sprache – ihre Einrichtungen und Regeln –, die ein ordnungsgemäßes Verständnis dieser Wertkategorien sowie Verweise darauf erfordern. Diese Aspekte umfassen das Übernehmen der Adresse eines Werts, das Kopieren eines Werts, das Verschieben eines Werts und das Weiterleiten eines Werts an eine andere Funktion. Dieses Thema befasst sich nicht mit all diesen Aspekten im Detail, sondern bietet grundlegende Informationen für ein solides Verständnis dieser Aspekte.
Die Informationen in diesem Thema werden im Rahmen von Stroustrups Analyse der Wertkategorien anhand der beiden unabhängigen Eigenschaften Identität und Bewegbarkeit dargestellt [Stroustrup, 2013].
Ein lvalue hat identität
Was bedeutet es, dass ein Wert Identität hat? Wenn Sie die Speicheradresse eines Werts haben (oder ermitteln können) und sicher damit umgehen können, dann hat der Wert eine Identität. Auf diese Weise können Sie mehr tun als den Inhalt von Werten zu vergleichen – Sie können sie anhand der Identität vergleichen oder unterscheiden.
Ein lvalue hat identität. Es geht jetzt nur um historisches Interesse, dass das "l" in "lvalue" eine Abkürzung von "left" ist (wie in, die linke Seite einer Aufgabe). In C++ kann ein l-Wert links oder rechts einer Zuweisung stehen. Das "l" in "lvalue" hilft Ihnen dann nicht, das zu verstehen oder zu definieren, was sie sind. Sie müssen nur verstehen, dass das, was wir einen L-Wert nennen, ein Wert ist, der eine Identität besitzt.
Beispiele für Ausdrücke, die lvalues sind, sind: eine benannte Variable oder Konstante; oder eine Funktion, die einen Verweis zurückgibt. Beispiele für Ausdrücke, die keine L-Werte sind, sind: ein temporärer Wert oder eine Funktion, die einen Wert zurückgibt.
int& get_by_ref() { ... }
int get_by_val() { ... }
int main()
{
std::vector<byte> vec{ 99, 98, 97 };
std::vector<byte>* addr1{ &vec }; // ok: vec is an lvalue.
int* addr2{ &get_by_ref() }; // ok: get_by_ref() is an lvalue.
int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is not an lvalue.
int* addr4{ &get_by_val() }; // Error: get_by_val() is not an lvalue.
}
Nun, es ist zwar richtig, dass lvalues eine Identität haben, das gilt jedoch auch für xvalues. Wir werden genau das, was ein X-Wert weiter unten in diesem Thema ist, eingehen. Beachten Sie jetzt einfach, dass es eine Wertkategorie namens glvalue (für "generalisierte lvalue ") gibt. Die Menge der glvalues ist die Obermenge sowohl der lvalues (auch als klassische lvalues bezeichnet) als auch der xvalues. Während „ein lvalue Identität besitzt“ zutrifft, ist die vollständige Menge der Dinge, die Identität besitzen, die Menge der glvalues, wie diese Abbildung zeigt.
Ein R-Wert ist verschiebbar; ein L-Wert ist es nicht
Es gibt jedoch Werte, die nicht glvalues sind. Mit anderen Worten: Es gibt Werte, für die Sie keine Speicheradresse abrufen können (oder sie können nicht darauf vertrauen, dass sie gültig ist). Wir haben einige solche Werte im obigen Codebeispiel gesehen.
Keine zuverlässige Speicheradresse klingt wie ein Nachteil. Aber in der Tat der Vorteil eines Werts wie das ist, dass Sie es verschieben können (was im Allgemeinen billig ist), anstatt sie zu kopieren (was im Allgemeinen teuer ist). Das Verschieben eines Werts bedeutet, dass er sich nicht mehr an der Stelle befindet, an der er verwendet wurde. Daher sollte vermieden werden, zu versuchen, dort darauf zuzugreifen, wo es sich früher befand. Eine Diskussion darüber, wann und wie ein Wert verschoben wird, liegt außerhalb des Gültigkeitsbereichs für dieses Thema. Für dieses Thema müssen wir nur wissen, dass ein beweglicher Wert als Rvalue (oder klassischer Rvalue) bezeichnet wird.
Das "r" in "rvalue" ist eine Abkürzung von "right" (wie in, die rechte Seite einer Aufgabe). Sie können jedoch rvalues und Referenzen auf rvalues auch außerhalb von Zuweisungen verwenden. Das „r“ in „rvalue“ ist also nicht das, worauf man sich konzentrieren sollte. Sie müssen nur verstehen, dass es sich bei dem, was wir als Wert bezeichnen, um einen beweglichen Wert handelt.
Ein L-Wert dagegen kann nicht verschoben werden, wie diese Abbildung zeigt. Wenn sich ein lvalue bewegen würde, würde das der eigentlichen Definition von lvalue widersprechen. Und es wäre ein unerwartetes Problem für Code, der vernünftigerweise davon ausgeht, weiterhin auf den lvalue zugreifen zu können.
Demnach können Sie einen Lvalue nicht verschieben. Es gibt jedoch eine Art glvalue (die Gruppe von Dingen mit Identität), die Sie verschieben können – wenn Sie wissen, was Sie tun (einschließlich der Vorsicht, dass Sie nach der Verschiebung nicht darauf zugreifen) – und das ist der X-Wert. Wir werden diese Idee später in diesem Thema noch einmal überprüfen, wenn wir uns das vollständige Bild der Wertkategorien ansehen.
Rvalue-Referenzen und Regeln für die Referenzbindung
In diesem Abschnitt wird die Syntax für einen Verweis auf einen rvalue vorgestellt. Wir müssen warten, bis wir ein anderes Thema ausführlicher behandeln, um uns eingehender mit Verschieben und Weiterleiten zu befassen; aber es genügt zu sagen, dass Rvalue-Referenzen ein notwendiger Bestandteil der Lösung dieser Probleme sind. Bevor wir uns jedoch mit Rvalue-Referenzen befassen, müssen wir uns zunächst klarer darüber werden, T&– über das, was wir bisher einfach nur als „eine Referenz“ bezeichnet haben. Es handelt sich tatsächlich um „eine Lvalue-Referenz (non-const)“, die auf einen Wert verweist, auf den der Nutzer der Referenz schreiben kann.
template<typename T> T& get_by_lvalue_ref() { ... } // Get by lvalue (non-const) reference.
template<typename T> void set_by_lvalue_ref(T&) { ... } // Set by lvalue (non-const) reference.
Eine Lvalue-Referenz kann an ein Lvalue binden, aber nicht an ein Rvalue.
Dann gibt es Lvalue-Konstverweise (T const&), die auf Objekte verweisen, auf die der Benutzer des Verweises nicht schreiben kann (z. B. eine Konstante).
template<typename T> T const& get_by_lvalue_cref() { ... } // Get by lvalue const reference.
template<typename T> void set_by_lvalue_cref(T const&) { ... } // Set by lvalue const reference.
Ein lvalue-Konstverweis kann an einen lvalue oder an einen Rvalue gebunden werden.
Die Syntax für einen Verweis auf einen rvalue des Typs T wird als T&& geschrieben. Ein Rvalue-Verweis bezieht sich auf einen beweglichen Wert – einen Wert, dessen Inhalt nicht beibehalten werden muss, nachdem wir ihn verwendet haben (z. B. ein temporärer Wert). Da es gerade darum geht, den an eine Rvalue-Referenz gebundenen Wert zu verschieben und ihn dabei zu verändern, gelten die Qualifizierer const und volatile (auch als cv-Qualifizierer bezeichnet) nicht für Rvalue-Referenzen.
template<typename T> T&& get_by_rvalue_ref() { ... } // Get by rvalue reference.
struct A { A(A&& other) { ... } }; // A move constructor takes an rvalue reference.
Ein Rvalue-Verweis bindet an einen Wert. Tatsächlich bevorzugt bei der Überladungsauflösung ein Rvalue eher die Bindung an eine Rvalue-Referenz als an eine konstante Lvalue-Referenz. Ein Rvalue-Verweis kann jedoch nicht an einen Lvalue gebunden werden, da ein Rvalue-Verweis, wie bereits erwähnt, auf einen Wert verweist, dessen Inhalt wir vermutlich nicht erhalten müssen (etwa den Parameter eines Move-Konstruktors).
Sie können auch einen Wert übergeben, bei dem ein Nachwertargument erwartet wird, über die Kopierkonstruktion (oder durch Verschieben der Konstruktion, wenn der Wert ein X-Wert ist).
Ein Glvalue hat Identität; ein Prvalue nicht
In dieser Phase wissen wir, was Identität hat. Und wir wissen, was verschiebbar ist und was nicht. Wir haben jedoch noch nicht den Satz von Werten benannt, die keine Identität haben. Diese Menge wird als prvalue oder pure rvalue bezeichnet.
int& get_by_ref() { ... }
int get_by_val() { ... }
int main()
{
int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is a prvalue.
int* addr4{ &get_by_val() }; // Error: get_by_val() is a prvalue.
}
Die vollständige Abbildung der Wertkategorien
Es bleibt nur, die oben genannten Informationen und Illustrationen in einem einzigen, großen Bild zu kombinieren.
glvalue (i)
Ein glvalue (generalisierter lvalue) hat eine Identität. Wir verwenden "i" als Kurzform für "hat Identität".
lvalue (i&!m)
Ein lvalue (eine Art glvalue) hat Identität, ist aber nicht verschiebbar. Dabei handelt es sich in der Regel um Werte zum Lesen und Schreiben, die Sie als Referenz, als konstante Referenz oder als Wert übergeben, wenn das Kopieren wenig Aufwand verursacht. Ein lvalue kann nicht an eine R-Wert-Referenz gebunden werden.
xvalue (i&m)
Ein xvalue (eine Art von glvalue, aber auch eine Art von rvalue) besitzt Identität und ist außerdem verschiebbar. Dies könnte ein zuvor vorhandener Lvalue sein, den Sie zu verschieben beschlossen haben, weil das Kopieren teuer ist, und auf den Sie danach sorgfältig nicht mehr zugreifen. Hier erfahren Sie, wie Sie einen Wert in einen X-Wert umwandeln können.
struct A { ... };
A a; // a is an lvalue...
static_cast<A&&>(a); // ...but this expression is an xvalue.
Im obigen Codebeispiel haben wir noch nichts verschoben. Wir haben lediglich einen X-Wert erstellt, indem wir einen lvalue in einen unbenannten Rvalue-Verweis umwandeln. Es kann weiterhin durch seinen Lvalue-Namen identifiziert werden; aber als X-Wert ist es jetzt in der Lage , verschoben zu werden. Die Gründe für das Verschieben und das Aussehen der Bewegung müssen auf ein anderes Thema warten. Aber Sie können das „x“ in „xvalue“ auch als „nur für Experten“ verstehen, wenn Ihnen das hilft. Durch Umwandlung eines Lvalues in einen X-Wert (eine Art von Rvalue, denken Sie daran), wird der Wert dann in der Lage, an einen Rvalue-Bezug gebunden zu werden.
Hier sind zwei weitere Beispiele für X-Werte: Aufrufen einer Funktion, die einen unbenannten Rvalue-Verweis zurückgibt und auf ein Element eines X-Werts zugreift.
struct A { int m; };
A&& f();
f(); // This expression is an xvalue...
f().m; // ...and so is this.
prvalue (!i&m)
Ein prvalue (reiner rvalue; eine Art von rvalue) hat keine Identität, ist aber verschiebbar. Dabei handelt es sich in der Regel um temporäre Objekte, um das Ergebnis des Aufrufs einer Funktion, die einen Wert zurückgibt, oder um das Ergebnis der Auswertung eines anderen Ausdrucks, der kein glvalue ist.
rvalue (m)
Ein rvalue ist bewegbar. Wir verwenden "m" als Kurzform für "ist verschiebbar".
Eine Rvalue-Referenz verweist immer auf einen Rvalue (einen Wert, dessen Inhalt wir vermutlich nicht erhalten müssen).
Aber, ist eine Rvalue-Referenz selbst ein Rvalue? Eine unbenannte Rvalue-Referenz (wie die oben in den xvalue-Codebeispielen gezeigten) ist ein xvalue und damit, ja, ein rvalue. Es empfiehlt sich, an einen Rvalue-Verweisfunktionsparameter wie die eines Verschiebungskonstruktors gebunden zu werden. Umgekehrt (und vielleicht gegen die Intuition): Wenn ein Rvalue-Bezug einen Namen hat, dann ist der Ausdruck, der aus diesem Namen gebildet wird, ein Lvalue. Es kann also nicht an einen Rvalue-Verweisparameter gebunden werden. Aber es ist einfach, es dazu zu bringen – wandeln Sie es einfach erneut in einen unbenannten Rvalue-Verweis (ein xvalue) um.
void foo(A&) { ... }
void foo(A&&) { ... }
void bar(A&& a) // a is a named rvalue reference; so it's an lvalue.
{
foo(a); // Calls foo(A&).
foo(static_cast<A&&>(a)); // Calls foo(A&&).
}
A&& get_by_rvalue_ref() { ... } // This unnamed rvalue reference is an xvalue.
!i&!m
Die Art des Werts, der keine Identität hat und nicht verschiebbar ist, ist die einzige Kombination, die wir noch nicht diskutiert haben. Aber wir können es ignorieren, da diese Kategorie keine nützliche Idee in der C++-Sprache ist.
Regeln zum Zusammenführen von Verweisen
Mehrere gleichartige Referenzen in einem Ausdruck (eine Lvalue-Referenz auf eine Lvalue-Referenz oder eine Rvalue-Referenz auf eine Rvalue-Referenz) heben sich gegenseitig auf.
-
A& &wird zuA&reduziert. -
A&& &&fällt zuA&&zusammen.
Mehrere ungleichartige Referenzen in einem Ausdruck werden zu einer Lvalue-Referenz.
-
A& &&wird zuA&. -
A&& &wird zuA&reduziert.
Weiterleitungsreferenzen
In diesem letzten Abschnitt werden Rvalue-Referenzen, die wir bereits behandelt haben, dem anderen Konzept einer Weiterleitungsreferenz gegenübergestellt. Bevor der Begriff „forwarding reference“ geprägt wurde, verwendeten einige den Begriff „universal reference“.
void foo(A&& a) { ... }
-
A&&ist eine Rvalue-Referenz, wie wir gesehen haben. Const und volatile gelten nicht für Rvalue-Referenzen. -
fooakzeptiert nur Werte vom Typ A. - Der Grund, warum Rvalue-Verweise (wie
A&&) existieren, ist, dass Sie eine Überladung schreiben können, die für den Fall optimiert ist, dass ein temporäres Objekt (oder ein anderer Rvalue) übergeben wird.
template <typename _Ty> void bar(_Ty&& ty) { ... }
-
_Ty&&ist eine Weiterleitungsreferenz. Je nachdem, was Sie anbarübergeben, kann der Typ _Ty unabhängig davon const bzw. nicht const sowie volatile bzw. nicht volatile sein. -
barakzeptiert alle Werte des Typs "lvalue" oder "rvalue" _Ty. - Wenn ein Lvalue übergeben wird, wird der Weiterleitungsverweis zu
_Ty& &&, was zu dem Lvalue-Verweis_Ty&reduziert wird. - Die Übergabe eines R-Werts bewirkt, dass die Forwarding-Referenz zu der R-Wert-Referenz
_Ty&&wird. - Der Grund, warum Weiterleitungsreferenzen (wie
_Ty&&) existieren, ist nicht die Optimierung, sondern dass sie dazu dienen, das, was Sie an sie übergeben, transparent und effizient weiterzureichen. Sie werden vermutlich nur dann auf eine Forwarding-Referenz stoßen, wenn Sie Bibliothekscode selbst schreiben (oder ihn eingehend analysieren) – beispielsweise eine Factory-Funktion, die Konstruktorargumente weiterreicht.
Sources
- [Stroustrup, 2013] B. Stroustrup: Die C++-Programmiersprache, Vierte Ausgabe. Addison-Wesley. 2013.
Windows developer