Catégories de valeurs et références à celles-ci

Cette rubrique présente et décrit les différentes catégories de valeurs (et les références aux valeurs) qui existent en C++ :

  • glvalue
  • lvalue
  • xlvalue
  • prvalue
  • rvalue

Vous avez certainement entendu parler de lvalues et de rvalues. Mais vous n’y pensez peut-être pas dans les termes présentés dans cette rubrique.

Chaque expression en C++ génère une valeur qui appartient à une des 5 catégories listées ci-dessus. Certains aspects du langage C++ (fonctionnalités et règles) nécessitent une bonne compréhension de ces catégories de valeurs et les références à celles-ci. Ces aspects sont par exemple prendre l’adresse d’une valeur, copier une valeur, déplacer une valeur et transférer une valeur à une autre fonction. Cette rubrique n’aborde pas tous ces aspects en détail, mais elle fournit des informations de base permettant de bien les comprendre.

Les informations fournies dans cette rubrique sont encadrées en termes d’analyse de catégories de valeur de Stroustrup par les deux propriétés indépendantes d’identité et de mobilité [Stroustrup, 2013].

Une lvalue a une identité

Que signifie, pour une valeur, d’avoir une identité ? Si vous avez (ou si vous pouvez prendre) l’adresse mémoire d’une valeur et l’utiliser de façon sécurisée, la valeur a une identité. Ainsi, vous pouvez effectuer bien plus que comparer le contenu de valeurs : vous pouvez les comparer ou les différencier par leur identité.

Une lvalue a une identité. Le fait que le « l » dans « lvalue » soit l’abréviation de « left » (gauche) (comme dans le côté gauche d’une assignation) ne présente désormais qu’un intérêt purement historique. En C++, une lvalue peut apparaître à gauche ou à droite d’une assignation. Le « l » dans « lvalues » ne vous permet pas vraiment de comprendre ni de définir ce qu’elles sont. Vous devez seulement comprendre que ce que nous appelons une lvalue est une valeur qui a une identité.

Une variable ou une constante nommée, ou encore une fonction qui retourne une référence constituent des exemples d’expressions qui sont des lvalues. Un élément temporaire ou une fonction qui produit un retour par valeur constituent des exemples d’expressions qui en sont pas des lvalues.

int& get_by_ref() { ... }
int get_by_val() { ... }

int main()
{
    std::vector<byte> vec{ 99, 98, 97 };
    std::vector<byte>* addr1{ &vec }; // ok: vec is an lvalue.
    int* addr2{ &get_by_ref() }; // ok: get_by_ref() is an lvalue.

    int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is not an lvalue.
    int* addr4{ &get_by_val() }; // Error: get_by_val() is not an lvalue.
}

S’il est vrai que les lvalues ont une identité, c’est également le cas des xvalues. Nous examinerons précisément ce qu’est une xvalue plus loin dans cette rubrique. Pour le moment, sachez simplement qu’il existe une catégorie de valeurs appelée glvalue, pour « generalized lvalue » (lvalue généralisée). L’ensemble des glvalues est le sur-ensemble des lvalues (également appelées lvalues classiques) et des xvalues. Par conséquent, même s’il est vrai qu’« une lvalue a une identité », l’ensemble complet des éléments ayant une identité est l’ensemble des glvalues, comme indiqué dans cette illustration.

An lvalue has identity

Une rvalue est déplaçable ; une lvalue ne l’est pas

Il y a toutefois des valeurs qui ne sont pas des glvalues. En d’autres termes, il existe des valeurs pour lesquelles vous ne pouvez pas obtenir une adresse mémoire (ou sur la validité desquelles vous ne pouvez pas compter). Nous avons vu de telles valeurs dans l’exemple de code ci-dessus.

L’absence d’une adresse mémoire fiable semble être un inconvénient. Mais, en réalité, l’avantage d’une valeur comme celle-ci est que vous pouvez la déplacer (ce qui est généralement économique) au lieu de la copier (ce qui est généralement coûteux). Déplacer une valeur signifie qu’elle n’est plus à l’emplacement où elle se trouvait habituellement. Par conséquent, il faut éviter de tenter d’y accéder à cet emplacement. Quand et comment déplacer une valeur n’est pas abordé dans cette rubrique. Pour cette rubrique, nous devons seulement savoir qu’une valeur déplaçable est appelée rvalue (ou rvalue classique).

Le « r » dans « rvalue » est l’abréviation de « right » (droite) comme dans le côté droit d’une assignation). Mais vous pouvez utiliser des rvalues et des références à des rvalues en dehors des affectations. Le « r » dans « rvalue » n’est donc pas le point sur lequel il faut se concentrer. Vous devez seulement comprendre que ce que nous appelons une rvalue est une valeur qui est déplaçable.

À l’inverse, une lvalue n’est pas déplaçable, comme le montre cette illustration. Si une lvalue devait se déplacer, cela contredirait la définition même de lvalue. Et ce serait un problème inattendu pour le code qui s’attend légitimement à pouvoir continuer à accéder à la lvalue.

An rvalue is movable; an lvalue is not

Vous ne pouvez donc pas déplacer une lvalue. Mais il existe une sorte de glvalue (l’ensemble d’éléments ayant une identité) que vous pouvez déplacer, si vous savez ce que vous faites (en veillant notamment à ne pas y accéder après le déplacement) : il s’agit de la xvalue. Nous reviendrons à ce concept plus loin dans cette rubrique, quand nous examinerons le panorama complet des catégories de valeurs.

Références rvalue et règles de liaison des références

Cette section présente la syntaxe d’une référence à une rvalue. Nous allons devoir attendre une autre rubrique pour traiter de manière substantielle le déplacement et le transfert, mais pour l’instant sachez que les références aux rvalue sont nécessaires à la solution de ces problèmes. Toutefois, avant d’étudier les références rvalue, nous devons d’abord clarifier ce qu’est T&, l’élément que nous avons auparavant simplement appelé « référence ». Il s’agit réellement d’« une référence lvalue (non-const) », qui fait référence à une valeur à laquelle l’utilisateur de la référence peut écrire.

template<typename T> T& get_by_lvalue_ref() { ... } // Get by lvalue (non-const) reference.
template<typename T> void set_by_lvalue_ref(T&) { ... } // Set by lvalue (non-const) reference.

Une référence lvalue peut établir une liaison à une lvalue, mais pas à une rvalue.

Ensuite, il existe des références const lvalue (T const&), qui font référence à des objets auxquels l’utilisateur de la référence ne peut pas écrire (par exemple, une constante).

template<typename T> T const& get_by_lvalue_cref() { ... } // Get by lvalue const reference.
template<typename T> void set_by_lvalue_cref(T const&) { ... } // Set by lvalue const reference.

Une référence const lvalue peut établir une liaison à une lvalue ou à une rvalue.

La syntaxe d’une référence à une rvalue de type T est écrite sous la forme T&&. Une référence rvalue fait référence à une valeur déplaçable, c’est-à-dire une valeur dont que nous n’avons pas besoin de conserver le contenu une fois que nous l’avons utilisée (par exemple, un élément temporaire). Étant donné que l’objectif principal est de se déplacer à partir la valeur liée à une référence rvalue (en la modifiant donc), les qualificateurs const et volatile (également appelés « qualificateurs cv ») ne s’appliquent pas aux références rvalue.

template<typename T> T&& get_by_rvalue_ref() { ... } // Get by rvalue reference.
struct A { A(A&& other) { ... } }; // A move constructor takes an rvalue reference.

Une référence rvalue établit une liaison à une rvalue. En réalité, en termes de résolution de surcharge, une rvalue préfère être liée à une référence rvalue plutôt qu’à une référence const lvalue. Mais une référence rvalue ne peut pas établir une liaison à une lvalue car, comme nous l’avons dit, une référence rvalue fait référence à une valeur dont on présume que nous n’avons pas besoin de conserver le contenu (par exemple, le paramètre d’un constructeur de déplacement).

Vous pouvez aussi passer une rvalue où un argument par valeur est attendu via une construction de copie (ou via une construction de déplacement si la rvalue est une xvalue).

Une glvalue a une identité ; une prvalue n’en a pas

À ce stade, nous savons ce qu’est une identité. Et nous savons ce qui est déplaçable et ce qui ne l’est pas. Mais nous n’avons pas encore nommé l’ensemble des valeurs qui n’ontpas d’identité. Cet ensemble est appelé prvalue, ou rvalue pure.

int& get_by_ref() { ... }
int get_by_val() { ... }

int main()
{
    int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is a prvalue.
    int* addr4{ &get_by_val() }; // Error: get_by_val() is a prvalue.
}

A glvalue has identity; a prvalue does not

Vue complète des catégories de valeurs

Il reste uniquement à regrouper les informations et les illustrations ci-dessus en une seule vue complète.

The complete picture of value categories

glvalue (i)

Une glvalue (lvalue généralisée) a une identité. Nous allons utiliser « i » comme raccourci pour « has identity » (a une identité).

lvalue (i&!m)

Une lvalue (sorte de glvalue) a une identité, mais ne peut pas être déplacée. Il s’agit de valeurs généralement en lecture-écriture que vous passez par référence ou par référence const, ou encore par valeur si la copie est économique. Une lvalue ne peut pas être liée à une référence rvalue.

xvalue (i&m)

Une xvalue (sorte de glvalue, mais également sorte de rvalue) a une identité et peut aussi être déplacée. Ce peut être une ancienne lvalue que vous avez décidé de déplacer car la copie est coûteuse, et vous veillerez à ne pas y accéder par la suite. Voici comment vous pouvez transformer une lvalue en xvalue.

struct A { ... };
A a; // a is an lvalue...
static_cast<A&&>(a); // ...but this expression is an xvalue.

Dans l’exemple de code ci-dessus, nous n’avons encore rien déplacé. Nous avons juste créé une xvalue en effectuant un cast d’une lvalue en une référence rvalue sans nom. Elle peut toujours être identifiée par son nom d’lvalue ; mais en tant que xvalue, elle est désormais capable d’être déplacée. Les raisons de la déplacer et ce à quoi ressemble réellement un déplacement feront l’objet d’une autre rubrique. Mais si cela vous aide, vous pouvez considérer que le « x » de « xvalue » signifie « expert seulement ». Grâce à un cast d’une lvalue en une xvalue (rappelez-vous, c’est une sorte de rvalue), la valeur peut maintenant être liée à une référence rvalue.

Voici deux autres exemples de xvalues : un appelant une fonction qui retourne une référence rvalue sans nom et un accédant à un membre d’une xvalue.

struct A { int m; };
A&& f();
f(); // This expression is an xvalue...
f().m; // ...and so is this.

prvalue (!i&m)

Une prvalue (rvalue pure ; sorte de rvalue) n’a pas d’identité, mais est déplaçable. Il s’agit généralement d’éléments temporaires, du résultat de l’appel d’une fonction qui retourne par valeur ou du résultat de l’évaluation de n’importe quelle autre expression qui n’est pas une glvalue.

rvalue (m)

Une rvalue est déplaçable. Nous allons utiliser « m » comme raccourci pour « is movable » (est déplaçable).

Une référence rvalue fait toujours référence à une rvalue (valeur dont on présume que nous n’avons pas besoin de conserver le contenu).

Mais une référence rvalue est-elle elle-même une rvalue ? Un référence rvalue sans nom (comme celles indiquées dans les exemples de code xvalue ci-dessus) est une xvalue. Donc, oui, il s’agit d’une rvalue. Elle préfère être liée à un paramètre de fonction de référence rvalue, comme celle d’un constructeur de déplacement. À l’inverse (et peut-être contre toute attente), si une référence rvalue a un nom, l’expression composée de ce nom est une lvalue. Par conséquent, elle ne peut pas être liée à un paramètre de référence rvalue. Mais il est facile de faire en sorte qu’elle le soit : il vous suffit d’en effectuer à nouveau un cast en une référence rvalue sans nom (une xvalue).

void foo(A&) { ... }
void foo(A&&) { ... }
void bar(A&& a) // a is a named rvalue reference; so it's an lvalue.
{
    foo(a); // Calls foo(A&).
    foo(static_cast<A&&>(a)); // Calls foo(A&&).
}
A&& get_by_rvalue_ref() { ... } // This unnamed rvalue reference is an xvalue.

!i&!m

Le type de valeur qui n’a pas identité et qui n’est pas déplaçable est la combinaison que nous n’avons pas encore abordée. Mais nous pouvons l’ignorer, car cette catégorie n’est pas un concept utile en langage C++.

Règles de réduction de référence

Plusieurs références similaires dans une expression (une référence lvalue à une référence lvalue, ou une référence rvalue à une référence rvalue) s’annulent mutuellement.

  • A& & est réduite en A&.
  • A&& && est réduite en A&&.

Plusieurs références différentes dans une expression sont réduites en une référence lvalue.

  • A& && est réduite en A&.
  • A&& & est réduite en A&.

Références de transfert

Cette dernière section compare des références rvalue, que nous avons déjà présentées, avec le nouveau concept de référence de transfert. Avant l’apparition du terme « référence de transfert », certaines personnes utilisaient le terme « référence universelle ».

void foo(A&& a) { ... }
  • A&& est une référence rvalue, comme nous l’avons vu. Const et volatile ne s’appliquent pas aux références rvalue.
  • foo accepte uniquement des rvalues de type A.
  • La raison de l’existence de références rvalue (comme A&&) est de vous permettre de créer une autre surcharge qui est optimisée en cas de passage d’un élément temporaire (ou d’une autre rvalue).
template <typename _Ty> void bar(_Ty&& ty) { ... }
  • _Ty&& est une référence de transfert. En fonction de ce que vous passez à bar, le type _Ty peut être const/non-const indépendamment de volatile/non-volatile.
  • bar accepte n’importe quelle lvalue ou rvalue de type _Ty.
  • Si une lvalue est passée, la référence de transfert devient _Ty& &&, qui est réduite en référence lvalue _Ty&.
  • Si une rvalue est passée, la référence de transfert devient la référence rvalue _Ty&&.
  • La raison de l’existence de références de transfert (comme _Ty&&) n’est pas l’optimisation, mais de prendre ce que vous leur passez et de le transférer de façon transparente et efficace. Vous rencontrerez probablement une référence de transfert uniquement si vous écrivez (ou étudiez attentivement) le code de la bibliothèque ; par exemple, une fonction d’usine qui effectue un transfert sur des arguments de constructeur.

Sources

  • [Stroustrup, 2013] B. Stroustrup: The C++ Programming Language, Fourth Edition. Addison-Wesley. (2013).