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.
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);
}
Une L-value est une expression dont la valeur est stockée dans une adresse en mémoire déterminée (pile ou tas).
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
}
une R-value est une expression dont la valeur n’est pas explicitement stockée en mémoire.
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
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.