Que se cache-t-il derrière les lettres l, r, x, gl et pr ? Décryptage des catégories de valeurs en C++
\n\nSi vous avez déjà écrit du code en C++ et obtenu une erreur comme "cannot bind non-const lvalue reference to an rvalue", vous avez déjà rencontré cette « magie ». Les catégories de valeurs — lvalue, rvalue, xvalue, glvalue et prvalue — ne sont pas qu'une simple classification académique. C'est le fondement sur lequel reposent la sémantique de déplacement (move semantics) et la transmission parfaite (perfect forwarding).
Décryptons une fois pour toutes ce que signifie chaque lettre, comment les distinguer en pratique et pourquoi sans cette connaissance, il est impossible d'écrire du C++ moderne et efficace.
\n\nBref historique : d'où viennent ces cinq termes ?
\n\nÀ l'origine, à l'époque du C, il n'y avait que deux concepts : lvalue (valeur gauche — ce qui peut se trouver à gauche d'une affectation) et rvalue (valeur droite — ce qui se trouve à droite). Tout était simple : une variable est une lvalue, un nombre est une rvalue.
\n\nAvec l'arrivée de C++11 et de la sémantique de déplacement, ce tableau simple a cessé de fonctionner. Les références sur rvalue (&&) sont apparues, ainsi que la nécessité de distinguer les objets temporaires « mourants ». C'est ainsi qu'est née la nouvelle classification, que la norme C++ décrit à travers cinq catégories.
Jetez un œil à la hiérarchie (diagramme textuel) :
\n\n expression\n / \\\n glvalue rvalue\n / \\ / \\\nlvalue xvalue prvalue\n\nMémorisez cette image — c'est la clé de tout.
\n\nlvalue (left value) — les objets « persistants »
\n\nlvalue est une expression qui fait référence à un objet ayant une adresse en mémoire. Cet objet ne disparaît pas après la fin de l'expression. Vous pouvez prendre son adresse, le modifier, et il continue d'exister.
\n\nExemples de lvalue :
\n- \n
- Nom de variable :
int x = 5; xest une lvalue. \n - Pointeur déréférencé :
*ptr. \n - Référence à une variable :
int& ref = x; ref. \n - Littéral de chaîne :
"hello"(oui, c'est une lvalue, bien que constante). \n
Code :
\nint main() {\n int a = 10;\n int* p = &a; // Prise d'adresse — ok, a est une lvalue\n int& ref = a; // Référence sur lvalue\n\n // &(a + 1); // Erreur ! a+1 est une rvalue, pas d'adresse\n return 0;\n}\n\nrvalue (right value) — les valeurs « jetables »
\n\nrvalue est une expression qui n'a pas d'adresse permanente en mémoire. Elle n'existe qu'au moment de l'évaluation de l'expression. Vous ne pouvez pas prendre son adresse (à de rares exceptions près).
\n\nLes rvalue incluent :
\n- \n
- Les littéraux (sauf les chaînes) :
42,3.14f,true. \n - Les résultats d'opérations arithmétiques :
a + b. \n - Les objets temporaires :
std::string("temp"). \n - Les expressions lambda. \n
Code :
\nint foo() { return 5; }\n\nint main() {\n int x = 42; // 42 — rvalue\n int y = x + 10; // x+10 — rvalue\n // int* p = &(x + 10); // Erreur ! Impossible de prendre l'adresse d'une rvalue\n return 0;\n}\n\nprvalue (pure rvalue) — les rvalue « pures »
\n\nprvalue est la rvalue classique de C++98. C'est une valeur temporaire qui n'a pas de nom et n'est pas liée à la mémoire. C'est ce que nous appelons généralement rvalue dans le langage courant.
\n\nExemples de prvalue :
\n- \n
- Nombres, énumérations, résultats d'opérateurs. \n
- Appel de fonction retournant une non-référence :
std::to_string(123). \n - Objet temporaire créé explicitement :
MyClass(). \n
xvalue (expiring value) — les objets « mourants »
\n\nC'est là que les choses deviennent vraiment intéressantes. xvalue est un objet qui va bientôt être détruit, mais dont les ressources peuvent encore être « sauvées » (déplacées). C'est un mo