L-value vs R-value

Sur cette page, on va parler des catégories des expressions (L-values et R-values), ce qui permet de mieux comprendre comment le compilateur les traite.

Remarque

La catégorisation des expressions est en fait encore plus complexe que celle décrite ici. Il existe en C++ des GL-values, PR-values et X-values, mais ces notions ne sont pas au programme du cours.


L-value

Pour illustrer ce que sont les L-values, on utilisera le bout de code ci-dessous.

class MyClass 
{
public:
  int my_att{};
};
 
int& my_func(const MyClass& y) { /* .. */ }

int main() 
{
    MyClass mon_instance;
//                  Expression 1
//                  vvvvvvvvvvvv
    int i = my_func(mon_instance);
//          ^^^^^^^^^^^^^^^^^^^^^
//          Expression 2 
//
//                 Expression 3
//          vvvvvvvvvvvvvvvvvvv
    return (mon_instance.my_att);
}

Important

Une L-value est une expression dont la valeur est stockée dans une adresse en mémoire déterminée (pile ou tas).

Etant donné une variable a, l’expression a est une L-value. Ceci est vrai que a soit une variable locale ou globale, et quel que soit le type déclaré de a. Par exemple, l’expression 1 mon_instance est une L-value dans le code ci-dessus. En effet dans ce cas, a n’est rien d’autre que le nom donné à une case mémoire. La plupart du temps, l’expression a ne va utiliser que la valeur stockée dans cette case mais parfois, c’est l’adresse de cette case qui est utilisée, comme dans l’expression &a, ou si a est passée par référence à une fonction (comme dans l’expression 2 my_func(mon_instance)).

Si une expression E est une L-value dont le type est une classe qui a un attribut c, alors E.c est aussi une L-value. Par exemple, l’expression 3 mon_instance.my_att est une L-value dans le code au dessus. En effet, comme expliqué dans le chapitre 3 les attributs d’un objet sont stockées dans l’espace allouée pour l’objet lui-même donc on sait où est chaque attribut est stockée dès qu’on sait où l’objet est stockée.

L’expression f(..) est une L-value si la fonction f renvoie une référence ou une référence constante, comme l’expression 3 my_func(mon_instance). C’est la même chose pour les fonction-membre ou les opérateurs.

Finalement, l’expression *E est une L-value dès lors que le type de E est un pointeur. En effet, on sait explicitement où cette valeur est stockée.

R-value

Pour illustrer ce que sont les R-values, on utilisera le bout de code ci-dessous.

class MyClass 
{
public:
  int my_att = 41;
};

int my_func(int x, const MyClass& y) { return 1; }

int main() 
{
//        Expression 1  Expression 2
//                  vv  vvvvvvvvv
    int i = my_func(42, MyClass{});
//          ^^^^^^^^^^^^^^^^^^^^^
//          Expression 3
//
//                   Expression 4
//             vvvvvvvvvvvvvvvvvv
    return i + (MyClass{}).my_att;
//         ^^^^^^^^^^^^^^^^^^^^^^
//         Expression 5
}

Important

une R-value est une expression dont la valeur n’est pas explicitement stockée en mémoire.

Par exemple, un littéral, comme 42 (expression 1 dans le code ci-dessus), n’est stocké nulle part en mémoire. En fait, le compilateur le stockera directement dans la case mémoire réservée pour l’argument x de la function my_func. C’est la même chose pour MyClass{} (expression 2), il s’agit d’une nouvelle instance de MyClass qui sera construite directement dans la case y de my_func.

Si une expression E est une R-value dont le type est une classe qui a un attribut c, alors R.c est aussi une R-value. C’est pour cela que (MyClass{}).my_att (l’expression 4) est une R-value.

L’expression f(..) est une R-value si la fonction f ne renvoie pas une référence, comme my_func(MyClass{}, 42) (expresion 3) dans le code ci-dessus. C’est la même chose pour les fonctions-membres ou les opérateurs (par exemple l’expression 5).

La fonction std::move

La fonction std::move permet de transformer une L-value en R-value. Nous verrons à quoi cela sert dans la section sur le déplacement de ce chapitre.

Pourquoi L et R ?

Historiquement les L et R dans L-value et R-value signifient “left” et “right” en référence à la position du signe = lors d’une affectation. Les R-value ne peuvent apparaître qu’à droite d’un = alors que les L-value peuvent apparaître à droite et à gauche d’un =.

Le bout de code ci-dessous illustre ceci pour les expressions 6, a et (a+6).

int a;

a = 6; // Cette affectation a un sens
a = (6+a); // Cette affectation a un sens
6 = a; // Cette affectation n'a aucun sens
(a+6) = a; // Cette affectation n'a aucun sens

// a peut apparaître à gauche de = donc c'est une L-value
// 6 ne peut apparaître qu'à droite du signe = donc c'est une R-value
// (a+6) ne peut apparaître qu'à droite du signe = donc c'est une R-value
Avertissement

Cette explication par left/right est un bon moyen mnémotechnique. Néanmoins ce n’est plus parfaitement alignée avec ce qu’il se passe en C++, car on peut entre autre, redéfinir le comportement des opérateurs d’affectations. Référez-vous plutôt aux définitions plus haut.

Chaînes de caractères littérales

Attention, les chaînes de caractères littérales, comme "Hello World!" sont des L-values comme le montre le code en dessous.

int main() {
   std::cout << &("Hello World") << " and " << &("Hello World") << std::endl;
//               ^^^^^^^^^^^^^^^^               ^^^^^^^^^^^^^^^^
// On peut demander l'addresse mémoire de chaînes de caractères litérales.
//
// Deux chaînes identiques dans le même module sont stockées au même endroit:
   std::cout << std::boolalpha << &("Hello World") ==  &("Hello World") << std::endl;
}

Ceci est une conséquence du string pooling, une optimisation dont le but est de réduire la taille du fichier binaire produit par le compilateur.