Ausdruckskategorien in C ++

Kategorien von Ausdrücken wie lvalue und rvalue beziehen sich mehr auf die grundlegenden theoretischen Konzepte der C ++ - Sprache als auf die praktischen Aspekte ihrer Verwendung. Aus diesem Grund haben viele sogar erfahrene Programmierer eine vage Vorstellung davon, was sie bedeuten. In diesem Artikel werde ich versuchen, die Bedeutung dieser Begriffe so einfach wie möglich zu erklären und die Theorie mit praktischen Beispielen zu verwässern. Ich werde sofort eine Reservierung vornehmen: Der Artikel gibt nicht vor, die vollständigste und strengste Beschreibung der Kategorien von Ausdrücken zu liefern. Für Details empfehle ich, sich direkt an die Quelle zu wenden: C ++ - Sprachstandard.


Der Artikel wird ziemlich viele englischsprachige Begriffe enthalten. Dies liegt an der Tatsache, dass einige von ihnen schwer ins Russische zu übersetzen sind, während andere auf unterschiedliche Weise in verschiedene Quellen übersetzt werden. Daher werde ich häufig englische Begriffe angeben und sie kursiv hervorheben.

Ein bisschen Geschichte


Die Begriffe lvalue und rvalue tauchten bereits in C auf. Es ist erwähnenswert, dass die Verwirrung ursprünglich in der Terminologie lag, da sie sich auf Ausdrücke und nicht auf Werte beziehen. Historisch gesehen kann ein l-Wert vom Zuweisungsoperator übrig bleiben, und ein r-Wert kann nur richtig sein .


lvalue = rvalue; 

Eine solche Definition vereinfacht und verzerrt jedoch das Wesentliche etwas. Der C89-Standard definierte lvalue als Objektlokalisator , d.h. Ein Objekt mit einem identifizierbaren Speicherort. Dementsprechend wurde alles, was nicht zu dieser Definition passte, in die Kategorie rvalue aufgenommen.


Bjarn eilt zur Rettung


In C ++ hat sich die Terminologie der Ausdruckskategorien ziemlich stark weiterentwickelt, insbesondere nach der Einführung des C ++ 11-Standards, in dem die Konzepte von rvalue- Links und Verschiebungssemantik eingeführt wurden. Die Geschichte der Entstehung neuer Terminologie wird interessanterweise in Straustrups Artikel „Neue“ Werteterminologie beschrieben .


Die neue, strengere Terminologie basiert auf zwei Eigenschaften:


  • das Vorhandensein von Identität ( Identität ) - das heißt, ein Parameter, anhand dessen verstanden werden kann, ob sich zwei Ausdrücke auf dieselbe Entität beziehen oder nicht (z. B. eine Adresse im Speicher);
  • Die Fähigkeit, sich zu bewegen ( kann verschoben werden ) - unterstützt die Semantik der Bewegung.

Identitätsausdrückende Ausdrücke werden unter dem Begriff glvalue ( verallgemeinerte Werte ) verallgemeinert , Roaming-Ausdrücke werden rvalue genannt . Kombinationen dieser beiden Eigenschaften haben 3 Hauptkategorien von Ausdrücken identifiziert:


Habe eine IdentitätOhne Identität
Kann nicht verschoben werdenlWert- -
Kann bewegt werdenxvalueWert

Tatsächlich führte der C ++ 17-Standard das Konzept der Kopierelision ein - formalisierende Situationen, in denen der Compiler das Kopieren und Verschieben von Objekten vermeiden kann und sollte. In dieser Hinsicht muss der Wert nicht unbedingt verschoben werden. Details und Beispiele finden Sie hier . Dies hat jedoch keinen Einfluss auf das Verständnis des allgemeinen Schemas der Kategorien von Ausdrücken.


Im modernen C ++ Standard wird die Kategoriestruktur in Form eines solchen Schemas dargestellt:


Bild


Lassen Sie uns allgemein die Eigenschaften von Kategorien sowie die Sprachausdrücke untersuchen, die in jeder der Kategorien enthalten sind. Ich stelle sofort fest, dass die unten aufgeführten Listen von Ausdrücken für jede Kategorie nicht als vollständig angesehen werden können. Für genauere und detailliertere Informationen sollten Sie sich direkt an den C ++ - Standard wenden.


glvalue


Ausdrücke in der Kategorie glvalue haben folgende Eigenschaften:


  • kann implizit in prvalue konvertiert werden ;
  • kann polymorph sein, das heißt, für sie sind die Konzepte eines statischen und dynamischen Typs sinnvoll;
  • kann nicht vom Typ void sein - dies folgt direkt aus der Eigenschaft, eine Identität zu haben, da es für Ausdrücke vom Typ void keinen solchen Parameter gibt, der sie voneinander unterscheiden würde;
  • kann einen unvollständigen Typ haben , z. B. in Form einer Vorwärtsdeklaration (sofern dies für einen bestimmten Ausdruck zulässig ist).

rWert


Ausdrücke in der Kategorie rvalue haben die folgenden Eigenschaften:


  • Sie können die r-Wert- Adresse nicht im Speicher abrufen - dies ergibt sich direkt aus dem Fehlen der Identitätseigenschaft.
  • darf sich nicht auf der linken Seite einer Zuweisung oder einer zusammengesetzten Zuweisungsanweisung befinden;
  • kann verwendet werden, um eine konstante lvalue- Verknüpfung oder rvalue- Verknüpfung zu initialisieren, während sich die Lebensdauer des Objekts auf die Lebensdauer der Verknüpfung erstreckt;
  • Wenn beim Aufrufen einer Funktion mit zwei überladenen Versionen ein Argument verwendet wird: Eine akzeptiert eine konstante l-Wert- Referenz und die andere eine r-Wert- Referenz. Dann wird die Version ausgewählt, die die r-Wert- Referenz akzeptiert. Mit dieser Eigenschaft wird die Verschiebungssemantik implementiert:

 class A { public: A() = default; A(const A&) { std::cout << "A::A(const A&)\n"; } A(A&&) { std::cout << "A::A(A&&)\n"; } }; ......... A a; A b(a); //  A(const A&) A c(std::move(a)); //  A(A&&) 

Technisch gesehen ist A && ein r-Wert und kann verwendet werden, um sowohl eine konstante l-Wert- Referenz als auch eine r- Wert- Referenz zu initialisieren. Dank dieser Eigenschaft besteht jedoch keine Mehrdeutigkeit. Es wird eine Konstruktoroption akzeptiert, die eine r-Wert- Referenz akzeptiert.

lWert


Eigenschaften:


  • alle glvalue- Eigenschaften (siehe oben);
  • Sie können die Adresse übernehmen (mit dem integrierten unären Operator & );
  • modifizierbare l-Werte können sich auf der linken Seite des Zuweisungsoperators oder der zusammengesetzten Zuweisungsoperatoren befinden;
  • kann verwendet werden, um eine Referenz auf einen l-Wert (sowohl konstant als auch nicht konstant) zu initialisieren.

Die folgenden Ausdrücke gehören zur Kategorie lvalue :


  • Der Name einer Variablen, Funktion oder eines Klassenfelds eines beliebigen Typs. Selbst wenn die Variable eine rWertreferenz ist , ist der Name dieser Variablen im Ausdruck ein lWert ;

 void func() {} ......... auto* func_ptr = &func; // :     auto& func_ref = func; // :     int&& rrn = int(123); auto* pn = &rrn; // :    auto& rn = rrn; // :  lvalue- 

  • Aufrufen einer Funktion oder eines überladenen Operators, der eine lWertreferenz oder einen Ausdruck der Konvertierung in den Typ einer lWertreferenz zurückgibt ;
  • integrierte Zuweisungsoperatoren, zusammengesetzte Zuweisungsoperatoren ( = , += , /= usw.), integriertes --b und --b ( ++a , --b ), integrierter Zeiger-Dereferenzierungsoperator ( *p );
  • eingebauter Operator für den Zugriff über den Index ( a[n] oder n[a] ), wenn einer der Operanden ein lvalue- Array ist;
  • Aufrufen einer Funktion oder einer überladenen Anweisung, die einen r-Wert- Verweis auf eine Funktion zurückgibt;
  • String-Literal wie "Hello, world!" .

Ein String-Literal unterscheidet sich von allen anderen Literalen in C ++ genau dadurch, dass es ein l-Wert ist (wenn auch unveränderlich). Zum Beispiel können Sie seine Adresse erhalten:

 auto* p = &”Hello, world!”; //   ,    

Wert


Eigenschaften:


  • alle rvalue- Eigenschaften (siehe oben);
  • kann nicht polymorph sein: statische und dynamische Ausdruckstypen fallen immer zusammen;
  • kann nicht von einem unvollständigen Typ sein (mit Ausnahme des Leertyps wird dies unten diskutiert);
  • kann keinen abstrakten Typ haben oder ein Array von Elementen eines abstrakten Typs sein.

Die folgenden Ausdrücke gehören zur Kategorie prvalue :


  • Literal (außer Zeichenfolge), zum Beispiel 42 , true oder nullptr ;
  • Ein Funktionsaufruf oder ein überladener Operator, der eine str.substr(1, 2) ( str.substr(1, 2) , str1 + str2 , it++ ) oder einen Konvertierungsausdruck in einen str1 + str2 (z. B. static_cast<double>(x) , std::string{} , (int)42 );
  • b-- und b-- ( a++ , b-- ), eingebaute mathematische Operationen ( a + b , a % b , a & b , a << b usw.), eingebaute logische Operationen ( a && b , a || b !a usw.), Vergleichsoperationen ( a < b , a == b , a >= b usw.), die eingebaute Operation zum Aufnehmen der Adresse ( &a );
  • dieser Zeiger;
  • Auflistung Artikel;
  • atypischer Vorlagenparameter, wenn es sich nicht um eine Klasse handelt;
  • Lambda-Ausdruck, zum Beispiel [](int x){ return x * x; } [](int x){ return x * x; } .

xvalue


Eigenschaften:


  • alle rvalue- Eigenschaften (siehe oben);
  • alle glvalue- Eigenschaften (siehe oben).

Beispiele für xvalue- Ausdrücke:


  • Aufrufen einer Funktion oder eines integrierten Operators, der eine r-Wert- Referenz zurückgibt , z. B. std :: move (x) ;

Tatsächlich können Sie für das Ergebnis des Aufrufs von std :: move () keine Adresse im Speicher abrufen oder eine Verknüpfung dazu initialisieren, aber gleichzeitig kann dieser Ausdruck polymorph sein:

 struct XA { virtual void f() { std::cout << "XA::f()\n"; } }; struct XB : public XA { virtual void f() { std::cout << "XB::f()\n"; } }; XA&& xa = XB(); auto* p = &std::move(xa); //  auto& r = std::move(xa); //  std::move(xa).f(); //  “XB::f()” 

  • Eingebauter Operator für den Zugriff nach Index ( a[n] oder n[a] ), wenn einer der Operanden ein rvalue- Array ist.

Einige Sonderfälle


Komma-Operator


Für den integrierten Kommaoperator stimmt die Ausdruckskategorie immer mit der Ausdruckskategorie des zweiten Operanden überein.


 int n = 0; auto* pn = &(1, n); // lvalue auto& rn = (1, n); // lvalue 1, n = 2; // lvalue auto* pt = &(1, int(123)); // , rvalue auto& rt = (1, int(123)); // , rvalue 

Leere Ausdrücke


Aufrufe von Funktionen, die void zurückgeben , Konvertierungsausdrücke in void umwandeln und Ausnahmen auslösen , werden als prvalue- Ausdrücke betrachtet, können jedoch nicht zum Initialisieren von Referenzen oder als Argumente für Funktionen verwendet werden.


Ternärer Vergleichsoperator


Definition der Ausdruckskategorie a ? b : c a ? b : c - der Fall ist nicht trivial, alles hängt von den Kategorien des zweiten und dritten Arguments ab ( b und c ):


  • Wenn b oder c vom Typ void sind , entsprechen die Kategorie und der Typ des gesamten Ausdrucks der Kategorie und dem Typ des anderen Arguments. Wenn beide Argumente vom Typ void sind , ist das Ergebnis ein Wert vom Typ void .
  • Wenn b und c Gl-Werte desselben Typs sind, ist das Ergebnis ein Gl-Wert desselben Typs.
  • In anderen Fällen ist das Ergebnis ein Wert.

Für den ternären Operator wird eine Reihe von Regeln definiert, nach denen implizite Konvertierungen auf die Argumente b und c angewendet werden können. Dies geht jedoch etwas über den Rahmen des Artikels hinaus. Wenn Sie interessiert sind, empfehle ich, auf den Abschnitt Bedingter Operator [Ausdruck] des Standards zu verweisen .


 int n = 1; int v = (1 > 2) ? throw 1 : n; // lvalue, .. throw   void,    n ((1 < 2) ? n : v) = 2; //  lvalue,  ,   ((1 < 2) ? n : int(123)) = 2; //   , ..    prvalue 

Verweise auf Felder und Methoden von Klassen und Strukturen


Für Ausdrücke der Form am und p->m (hier geht es um den eingebauten Operator -> ) gelten folgende Regeln:


  • Wenn m ein Aufzählungselement oder eine nicht statische Klassenmethode ist, wird der gesamte Ausdruck als prvalue betrachtet (obwohl die Verknüpfung mit einem solchen Ausdruck nicht initialisiert werden kann).
  • Wenn a ein rWert und m ein nicht statisches Feld eines Nichtreferenztyps ist, gehört der gesamte Ausdruck zur Kategorie xWert .
  • sonst ist es ein Wert .

Für Zeiger auf Klassenmitglieder ( a.*mp und p->*mp ) gelten folgende Regeln:


  • Wenn mp ein Zeiger auf eine Klassenmethode ist, wird der gesamte Ausdruck als prvalue betrachtet .
  • Wenn a ein r-Wert ist und mp ein Zeiger auf ein Datenfeld ist, bezieht sich der gesamte Ausdruck auf x-Wert .
  • sonst ist es ein Wert .

Bitfelder


Bitfelder sind ein praktisches Werkzeug für die Programmierung auf niedriger Ebene, ihre Implementierung liegt jedoch etwas außerhalb der allgemeinen Struktur von Ausdruckskategorien. Beispielsweise scheint ein Aufruf eines Bitfelds ein Wert zu sein , da er möglicherweise auf der linken Seite des Zuweisungsoperators vorhanden ist. Gleichzeitig funktioniert es nicht, die Adresse des Bitfelds zu übernehmen oder eine nicht konstante Verbindung durch diese zu initialisieren. Sie können einen konstanten Verweis auf ein Bitfeld initialisieren, es wird jedoch eine temporäre Kopie des Objekts erstellt:


Bitfelder [class.bit]
Wenn der Initialisierer für eine Referenz vom Typ const T & ein Wert ist, der sich auf ein Bitfeld bezieht, ist die Referenz an eine temporäre Initialisierung gebunden, die den Wert des Bitfelds enthält. Die Referenz ist nicht direkt an das Bitfeld gebunden.

 struct BF { int f:3; }; BF b; bf = 1; // OK auto* pb = &b.f; //  auto& rb = bf; //  

Anstelle einer Schlussfolgerung


Wie ich in der Einleitung erwähnt habe, erhebt die obige Beschreibung keinen Anspruch auf Vollständigkeit, sondern gibt nur einen allgemeinen Überblick über die Kategorien von Ausdrücken. Diese Ansicht bietet ein etwas besseres Verständnis der Absätze des Standards und der Compiler-Fehlermeldungen.

Source: https://habr.com/ru/post/de441742/


All Articles