Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Cette rubrique présente et décrit les différentes catégories de valeurs (et références aux valeurs) qui existent en C++ :
- glvalue
- lvalue
- xlvalue
- prvalue
- rvalue
Vous aurez sans doute entendu parler de lvalues et de rvalues. Mais vous ne pensez peut-être pas à eux dans les termes présentés par cette rubrique.
Chaque expression en C++ génère une valeur qui appartient à l’une des cinq catégories répertoriées ci-dessus. Il existe des aspects du langage C++, ses installations et ses règles, qui demandent une bonne compréhension de ces catégories de valeurs, ainsi que des références à ces catégories de valeurs. Ces aspects incluent la prise de l’adresse d’une valeur, la copie d’une valeur, le déplacement d’une valeur et le transfert d’une valeur vers une autre fonction. Cette rubrique n’entre pas dans tous ces aspects en profondeur, mais elle fournit des informations fondamentales pour une bonne compréhension de ces aspects.
Les informations contenues dans cette rubrique sont encadrées en termes d’analyse des catégories de valeurs de Stroustrup par les deux propriétés indépendantes de l’identité et de la movabilité [Stroustrup, 2013].
Une lvalue a une identité
Qu’est-ce que cela signifie pour qu’une valeur ait une identité ? Si vous avez (ou vous pouvez prendre) l’adresse mémoire d’une valeur et que vous l’utilisez en toute sécurité, la valeur a une identité. De cette façon, vous pouvez faire plus que comparer le contenu des valeurs , vous pouvez les comparer ou les distinguer par identité.
Une valeur lvalue a une identité. C’est maintenant une question d’intérêt historique que le « l » dans « lvalue » est une abréviation de « left » (comme dans, le côté gauche d’une affectation). En C++, une lvalue peut apparaître à gauche ou à droite d’une affectation. Le « l » dans « lvalue », alors, ne vous aide pas réellement à comprendre ni à définir ce qu’ils sont. Vous devez uniquement comprendre que ce que nous appelons une valeur lvalue est une valeur qui a une identité.
Voici quelques exemples d’expressions qui sont des valeurs lvalues : une variable nommée ou une constante ; ou une fonction qui retourne une référence. Voici quelques exemples d’expressions qui ne sont pas des lvalues : une valeur temporaire ou une fonction qui renvoie par valeur.
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.
}
Or, s’il est vrai que les lvalues ont une identité, c’est également le cas des xvalues. Nous verrons plus loin dans cette section exactement ce qu’est une xvalue. Pour l’instant, sachez simplement qu’il existe une catégorie de valeur appelée glvalue (pour « lvalue généralisée »). L’ensemble de glvalues est le superset des valeurs lvalues (également appelées lvalues classiques) et xvalues. Ainsi, alors que « une lvalue a une identité » est vraie, l’ensemble complet des éléments qui ont l’identité est l’ensemble de glvalues, comme illustré dans cette illustration.
Une rvalue est déplaçable ; une lvalue ne l’est pas
Mais certaines valeurs ne sont pas des glvalues. En d’autres termes, il existe des valeurs dont vous ne pouvez pas obtenir l’adresse mémoire (ou dont vous ne pouvez pas garantir la validité). Nous avons vu certaines valeurs de ce type dans l’exemple de code ci-dessus.
L’absence d’adresse mémoire fiable ressemble à un inconvénient. Mais en fait, l’avantage d’une valeur comme celle-ci est que vous pouvez le déplacer (ce qui est généralement bon marché), plutôt que de le copier (qui est généralement coûteux). Le déplacement d’une valeur signifie qu’elle n’est plus à l’endroit où elle était utilisée. Il faut donc éviter d’essayer d’y accéder à l’endroit où il se trouvait auparavant. Une discussion sur le moment et la façon de déplacer une valeur est hors de portée pour cette rubrique. Pour cette rubrique, nous devons simplement savoir qu’une valeur mobile est appelée rvalue (ou rvalue classique).
Le « r » dans « rvalue » est une abréviation de « droite » (comme dans le cas, côté droit d’une affectation). Toutefois, vous pouvez utiliser des rvalues et des références à des rvalues en dehors des opérations d’affectation. Le « r » dans « rvalue » n’est donc pas l’élément sur lequel il faut se concentrer. Il vous suffit de comprendre que ce que nous appelons une valeur rvalue est une valeur mobile.
Une lvalue, à l’inverse, n’est pas mobile, comme illustré dans cette illustration. Si une lvalue devait se déplacer, cela contredirait la définition même de lvalue. Et il s’agirait d’un problème inattendu pour le code qui devrait très raisonnablement pouvoir continuer à accéder à la valeur lvalue.
Vous ne pouvez donc pas déplacer une lvalue. Mais il existe une sorte de glvalue (l’ensemble des objets dotés d’une identité) que vous pouvez déplacer — si vous savez ce que vous faites (notamment en veillant à ne plus y accéder après le déplacement) — et c’est ce qu’on appelle une xvalue. Nous allons revoir cette idée une fois plus tard dans cette rubrique lorsque nous examinons l’image complète des catégories de valeurs.
Références Rvalue et règles de liaison de référence
Cette section présente la syntaxe d’une référence à une valeur rvalue. Nous devrons attendre un autre sujet pour aborder en détail le déplacement et le forwarding, mais qu’il suffise de dire que les références rvalue sont un élément indispensable pour résoudre ces problèmes. Avant d’examiner les références rvalue, toutefois, nous devons d’abord préciser ce que T& — ce que nous avons jusqu’ici appelé simplement « une référence » — désigne. Il s’agit vraiment 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 être liée à une valeur lvalue, mais pas à une valeur rvalue.
Il existe ensuite des références lvalue const (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 lvalue const peut être liée à une valeur lvalue ou à une valeur rvalue.
La syntaxe d’une référence à une rvalue de type T est écrite en tant que T&&. Une référence rvalue fait référence à une valeur mobile : valeur dont nous n’avons pas besoin de conserver le contenu après l’avoir utilisé (par exemple, temporaire). Puisque tout l’intérêt est de déplacer (et donc de modifier) la valeur liée à une référence rvalue, 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 est liée à une valeur rvalue. En fait, en termes de résolution de surcharge, une rvalue préfère être liée à une référence rvalue qu’à une référence lvalue const. Mais une référence rvalue ne peut pas être liée à une valeur lvalue, car, comme nous l’avons dit, une référence rvalue fait référence à une valeur dont le contenu est supposé que nous n’avons pas besoin de conserver (par exemple, le paramètre d’un constructeur de déplacement).
Vous pouvez également passer une valeur rvalue où un argument par valeur est attendu, via la construction de copie (ou via la construction de déplacement si la valeur rvalue est une valeur xvalue).
Une glvalue a une identité ; une prvalue n’a pas d’identité
À ce stade, nous savons ce qui a une identité. Et nous savons ce qui est mobile et ce qui n’est pas. Mais nous n’avons pas encore nommé l’ensemble de valeurs qui n’ont pas d’identité . Cet ensemble est appelé prvalue, ou pure rvalue.
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.
}
Image complète des catégories de valeurs
Il reste seulement à combiner les informations et les illustrations ci-dessus en une seule et grande image.
glvalue (i)
Une glvalue (lvalue généralisée) a une identité. Nous utiliserons « i » comme abréviation de « possède une identité ».
lvalue (i&!m)
Une lvalue (un type de glvalue) a une identité, mais n’est pas mobile. Il s’agit généralement de valeurs modifiables que vous transmettez par référence, par référence constante ou par valeur si la copie est peu coûteuse. Une lvalue ne peut pas être liée à une référence rvalue.
xvalue (i&m)
Une xvalue (un type de glvalue, mais aussi un type de rvalue) a une identité et est également mobile. Il peut s’agir d’une lvalue auparavant ordinaire que vous avez décidé de déplacer parce que la copie est coûteuse, et vous veillerez à ne plus y accéder par la suite. Voici comment transformer une valeur 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 pas encore déplacé quoi que ce soit. Nous avons simplement créé une xvalue en convertissant une lvalue en une référence rvalue non nommée. Il peut toujours être identifié par son nom lvalue ; mais, en tant que xvalue, il est maintenant capable d’être déplacé. Les raisons de le déplacer, et ce à quoi ressemble réellement ce déplacement, devront attendre qu’on aborde un autre sujet. Mais vous pouvez considérer le « x » dans « xvalue » comme signifiant « expert uniquement » si cela vous aide. En cas de conversion d’une valeur lvalue en xvalue (un type de rvalue, n’oubliez pas), la valeur devient alors capable d’être liée à une référence rvalue.
Voici deux autres exemples de xvalues : l’appel d’une fonction qui retourne une référence rvalue sans nom et l’accès à un membre d’une valeur 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 ; un type de rvalue) n’a pas d’identité, mais est mobile. Ils sont généralement temporaires, ou le résultat de l’appel d’une fonction qui retourne par valeur, ou le résultat de l’évaluation d’une autre expression qui n’est pas une glvalue.
rvalue (m)
Une valeur rvalue est mobile. Nous allons utiliser « m » comme raccourci pour « est mobile ».
Une référence de rvalue désigne toujours une rvalue (une valeur dont on suppose qu’il n’est pas nécessaire de préserver le contenu).
Mais une référence de valeur droite est-elle elle-même une valeur droite ? Une référence rvalue non nommée (comme celles affichées dans les exemples de code xvalue ci-dessus) est une valeur xvalue. Oui, il s’agit d’une valeur rvalue. Il préfère être lié à un paramètre de fonction de référence rvalue, tel que celui d’un constructeur de déplacement. Inversement (et peut-être contre-intuitivement), si une référence rvalue a un nom, l’expression composée de ce nom est une lvalue. Il ne peut donc pas être lié à un paramètre de référence rvalue. Mais il est facile de lui redonner ce statut : il suffit de le convertir de nouveau en une référence rvalue non nommée (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 d’identité et qui n’est pas mobile est la combinaison que nous n’avons pas encore abordée. Mais nous pouvons l’ignorer, car cette catégorie n’est pas une idée utile dans le langage C++.
Règles de réduction des références
Plusieurs références de même type 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& &se réduit àA&. -
A&& &&se réduit àA&&.
Plusieurs références de types différents dans une expression se réduisent à une référence de lvalue.
-
A& &&se réduit àA&. -
A&& &se réduit àA&.
Références de transfert
Cette dernière section contraste les références rvalue, que nous avons déjà abordées, avec le concept différent d’une référence de transfert. Avant que le terme « référence de transfert » ne soit inventé, 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. -
fooaccepte uniquement les valeurs rvalues de type A. - Le but des références rvalue (telles que
A&&) est de vous permettre de définir une surcharge optimisée pour le cas où une valeur temporaire (ou une autre rvalue) est passée en argument.
template <typename _Ty> void bar(_Ty&& ty) { ... }
-
_Ty&&est une référence de réacheminement. Selon ce que vous passez àbar, le type _Ty peut être const ou non-const, indépendamment du fait qu’il soit volatile ou non-volatile. -
baraccepte n’importe quelle valeur lvalue ou rvalue de type _Ty. - Le passage d’une lvalue fait que la référence de transfert devient
_Ty& &&, qui se réduit en référence lvalue_Ty&. - Le fait de passer une rvalue fait que la référence de renvoi devient la référence rvalue
_Ty&&. - La raison pour laquelle les références de transfert (par exemple
_Ty&&) n’existent pas pour l’optimisation, mais pour prendre ce que vous leur transmettez et pour la transférer de manière transparente et efficace. Vous ne rencontrerez probablement une référence de redirection que si vous écrivez (ou étudiez de près) du code de bibliothèque — par exemple, une fonction de fabrique qui redirige les arguments vers le constructeur.
Sources
- [Stroustrup, 2013] B. Stroustrup : The C++ Programming Language, Fourth Edition. Addison-Wesley. 2013.
Windows developer