Catégories d'expressions en C ++

Les catégories d'expressions, telles que lvalue et rvalue , se rapportent davantage aux concepts théoriques fondamentaux du langage C ++ qu'aux aspects pratiques de son utilisation. Pour cette raison, de nombreux programmeurs même expérimentés ont une vague idée de ce qu'ils signifient. Dans cet article, je vais essayer d'expliquer la signification de ces termes aussi simple que possible, en diluant la théorie avec des exemples pratiques. Je ferai une réserve tout de suite: l'article ne prétend pas fournir la description la plus complète et rigoureuse des catégories d'expressions; pour plus de détails, je recommande de contacter directement la source: standard du langage C ++.


L'article contiendra beaucoup de termes en anglais, cela est dû au fait que certains d'entre eux sont difficiles à traduire en russe, tandis que d'autres sont traduits dans différentes sources de différentes manières. Par conséquent, j'indiquerai souvent des termes anglais, en les surlignant en italique .

Un peu d'histoire


Les termes lvalue et rvalue sont réapparus en C. Il convient de noter que la confusion était initialement présente dans la terminologie, car ils se réfèrent à des expressions et non à des valeurs. Historiquement, une lvalue est ce qui peut être laissé de l'opérateur d'affectation, et une rvalue est ce qui ne peut être que droit .


lvalue = rvalue; 

Cependant, une telle définition simplifie et déforme quelque peu l'essence. La norme C89 définit lvalue comme un localisateur d'objets , c'est-à-dire Un objet avec un emplacement mémoire identifiable. Par conséquent, tout ce qui ne correspondait pas à cette définition a été inclus dans la catégorie valeur.


Bjarn se précipite à la rescousse


En C ++, la terminologie des catégories d'expression a évolué assez fortement, surtout après l'adoption de la norme C ++ 11, qui a introduit les concepts de liens rvalue et de sémantique de déplacement . L'histoire de l'émergence d'une nouvelle terminologie est décrite de manière intéressante dans l'article de Straustrup «New» Value Terminology .


La nouvelle terminologie, plus rigoureuse, repose sur 2 propriétés:


  • la présence d'identité ( identité ), c'est-à-dire un paramètre permettant de comprendre si deux expressions se réfèrent ou non à la même entité (par exemple, une adresse en mémoire);
  • la capacité de se déplacer ( peut être déplacé ) - prend en charge la sémantique du mouvement.

Les expressions exprimant une identité sont généralisées sous le terme glvalue ( valeurs généralisées ), les expressions itinérantes sont appelées rvalue . Les combinaisons de ces deux propriétés ont identifié 3 catégories principales d'expressions:


Avoir une identitéDépourvu d'identité
Ne peut pas être déplacélvalue-
Peut être déplacéxvaluevaleur

En fait, la norme C ++ 17 a introduit le concept d' élision de copie - formaliser des situations où le compilateur peut et doit éviter de copier et de déplacer des objets. À cet égard, la valeur ne peut pas nécessairement être déplacée. Des détails et des exemples peuvent être trouvés ici . Cependant, cela n'affecte pas la compréhension du schéma général des catégories d'expressions.


Dans la norme C ++ moderne, la structure des catégories est présentée sous la forme d'un tel schéma:


image


Examinons en termes généraux les propriétés des catégories, ainsi que les expressions de langage qui sont incluses dans chacune des catégories. Je note tout de suite que les listes d'expressions ci-dessous pour chaque catégorie ne peuvent pas être considérées comme complètes; pour des informations plus précises et détaillées, reportez-vous directement à la norme C ++.


glvalue


Les expressions de la catégorie glvalue ont les propriétés suivantes:


  • peut être implicitement converti en valeur ;
  • peut être polymorphe, c'est-à-dire que pour eux les concepts de type statique et dynamique ont un sens;
  • ne peut pas être de type void - cela découle directement de la propriété d'avoir une identité, car pour les expressions de type void , aucun paramètre de ce type ne les distinguerait les unes des autres;
  • peut avoir un type incomplet , par exemple, sous la forme d'une déclaration directe (si autorisé pour une expression particulière).

rvalue


Les expressions de la catégorie rvalue ont les propriétés suivantes:


  • vous ne pouvez pas obtenir l'adresse rvalue en mémoire - cela découle directement du manque de propriété d'identité;
  • ne peut pas être sur le côté gauche d'une affectation ou d'une instruction d'affectation composée;
  • peut être utilisé pour initialiser un lien lvalue constant ou un lien rvalue , tandis que la durée de vie de l'objet s'étend à la durée de vie du lien;
  • s'il est utilisé comme argument lors de l'appel d'une fonction qui a 2 versions surchargées: l'une accepte une référence lvalue constante et l'autre une référence rvalue , alors la version qui accepte la référence rvalue est sélectionnée. C'est cette propriété qui est utilisée pour implémenter la sémantique de déplacement :

 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&&) 

Techniquement, A && est une rvalue et peut être utilisé pour initialiser à la fois une référence lvalue constante et une référence rvalue . Mais grâce à cette propriété, il n'y a pas d'ambiguïté; une option constructeur est acceptée qui accepte une référence rvalue .

lvalue


Propriétés:


  • toutes les propriétés glvalue (voir ci-dessus);
  • vous pouvez prendre l'adresse (en utilisant l'opérateur unaire intégré & );
  • les valeurs l modifiables peuvent être sur le côté gauche de l'opérateur d'affectation ou des opérateurs d'affectation composés;
  • peut être utilisé pour initialiser une référence à une valeur l (constante et non constante).

Les expressions suivantes appartiennent à la catégorie lvalue :


  • le nom d'une variable, d'une fonction ou d'un champ de classe de tout type. Même si la variable est une référence rvalue , le nom de cette variable dans l'expression est une lvalue ;

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

  • appeler une fonction ou un opérateur surchargé qui renvoie une référence lvalue , ou une expression de conversion au type d'une référence lvalue ;
  • opérateurs d'affectation intégrés, opérateurs d'affectation composés ( = , += , /= , etc.), pré-incrémentation et pré-incrémentation intégrés ( ++a , --b ), opérateur de déréférence de pointeur intégré ( *p );
  • opérateur d'accès intégré par index ( a[n] ou n[a] ), lorsque l'un des opérandes est un tableau lvalue ;
  • appeler une fonction ou une instruction surchargée qui renvoie une référence rvalue à une fonction;
  • chaîne littérale telle que "Hello, world!" .

Un littéral de chaîne diffère de tous les autres littéraux en C ++ précisément en ce qu'il est une valeur l (bien qu'immuable). Par exemple, vous pouvez obtenir son adresse:

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

valeur


Propriétés:


  • toutes les propriétés rvalue (voir ci-dessus);
  • ne peut pas être polymorphe: les types d'expression statique et dynamique coïncident toujours;
  • ne peut pas être de type incomplet (sauf pour le type void , cela sera discuté ci-dessous);
  • ne peut pas avoir un type abstrait ou être un tableau d'éléments d'un type abstrait.

Les expressions suivantes appartiennent à la catégorie prvalue :


  • littéral (sauf chaîne), par exemple 42 , true ou nullptr ;
  • un appel de fonction ou un opérateur surchargé qui renvoie une non-référence ( str.substr(1, 2) , str1 + str2 , it++ ) ou une expression de conversion en un type non-référence (par exemple, static_cast<double>(x) , std::string{} , (int)42 );
  • post-incrémentation et post-décrémentation b-- ( a++ , b-- ), opérations mathématiques intégrées ( a + b , a % b , a & b , a << b , etc.), opérations logiques intégrées ( a && b , a || b !a , etc.), les opérations de comparaison ( a < b , a == b , a >= b , etc.), l'opération intégrée de prise de l'adresse ( &a );
  • ce pointeur;
  • élément de liste;
  • paramètre de modèle atypique, s'il ne s'agit pas d'une classe;
  • expression lambda, par exemple [](int x){ return x * x; } [](int x){ return x * x; } .

xvalue


Propriétés:


  • toutes les propriétés rvalue (voir ci-dessus);
  • toutes les propriétés glvalue (voir ci-dessus).

Exemples d'expressions xvalue :


  • appeler une fonction ou un opérateur intégré qui renvoie une référence rvalue , par exemple std :: move (x) ;

et en fait, pour le résultat de l'appel à std :: move (), vous ne pouvez pas obtenir une adresse en mémoire ou initialiser un lien vers celle-ci, mais en même temps, cette expression peut être polymorphe:

 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()” 

  • opérateur d'accès intégré par index ( a[n] ou n[a] ) lorsque l'un des opérandes est un tableau rvalue .

Quelques cas particuliers


Opérateur virgule


Pour l'opérateur de virgule intégré, la catégorie d'expression correspond toujours à la catégorie d'expression du deuxième opérande.


 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 

Expressions nulles


Les appels à des fonctions qui renvoient void , saisissent des expressions de conversion à void et lèvent des exceptions sont considérés comme des expressions de valeur, mais ils ne peuvent pas être utilisés pour initialiser des références ou comme arguments de fonctions.


Opérateur de comparaison ternaire


Définition de la catégorie d'expression a ? b : c a ? b : c - le cas n'est pas trivial, tout dépend des catégories des deuxième et troisième arguments ( b et c ):


  • si b ou c sont de type void , alors la catégorie et le type de l'expression entière correspondent à la catégorie et au type de l'autre argument. Si les deux arguments sont de type void , le résultat est une valeur de type void ;
  • si b et c sont des valeurs de gl du même type, alors le résultat est une valeur de gl du même type;
  • dans d'autres cas, le résultat est une valeur.

Pour l'opérateur ternaire, un certain nombre de règles sont définies selon lesquelles des conversions implicites peuvent être appliquées aux arguments b et c, mais cela dépasse quelque peu la portée de l'article; si vous êtes intéressé, je vous recommande de vous référer à la section Opérateur conditionnel [expr.cond] de la norme.


 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 

Références aux domaines et aux méthodes des classes et des structures


Pour les expressions de la forme am et p->m (nous parlons ici de l'opérateur intégré -> ), les règles suivantes s'appliquent:


  • si m est un élément d'énumération ou une méthode de classe non statique, alors l'expression entière est considérée comme une valeur (bien que le lien ne puisse pas être initialisé avec une telle expression);
  • si a est une valeur r et que m est un champ non statique d'un type non référence, alors l'expression entière appartient à la catégorie xvalue ;
  • sinon c'est une valeur l .

Pour les pointeurs vers les membres de la classe ( a.*mp et p->*mp ), les règles sont similaires:


  • si mp est un pointeur vers une méthode de classe, alors l'expression entière est considérée comme une valeur ;
  • si a est une valeur r et que mp est un pointeur vers un champ de données, alors l'expression entière se réfère à la valeur x ;
  • sinon c'est une valeur l .

Champs de bits


Les champs de bits sont un outil pratique pour la programmation de bas niveau, cependant, leur implémentation sort quelque peu de la structure générale des catégories d'expression. Par exemple, un appel à un champ de bits semble être une valeur l , car il peut être présent sur le côté gauche de l'opérateur d'affectation. En même temps, cela ne fonctionnera pas pour prendre l'adresse du champ de bits ou initialiser une liaison non constante par eux. Vous pouvez initialiser une référence constante à un champ de bits, mais une copie temporaire de l'objet sera créée:


Champs de bits [class.bit]
Si l'initialiseur pour une référence de type const T & est une valeur l qui se réfère à un champ de bits, la référence est liée à un temporaire initialisé pour contenir la valeur du champ de bits; la référence n'est pas directement liée au champ binaire.

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

Au lieu d'une conclusion


Comme je l'ai mentionné dans l'introduction, la description ci-dessus ne prétend pas être complète, mais donne seulement une idée générale des catégories d'expressions. Cette vue permettra de mieux comprendre les paragraphes de la norme et les messages d'erreur du compilateur.

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


All Articles