plus particulièrement: Références sur les R-value
Directory
Il existe d’autres -values que les deux présentés ici. Pour plus d’info: cppreference
Malgré la présence du mot “valeur” dans le nom l-value **et **r-value ne sont en réalité pas des valeurs mais plutôt des propriétés des expressions.
Toute expression en C++ à deux propriétés:
- Un type: Utilisé pour le typage et la vérification)
- Une catégorie de valeur: Utilisé pour certains types de vérification syntaxique comme “est-ce que le résultat de cette expression peut être assigné ici”
La défintion formelle de ce qu’est une L-value et une R-value est largement audessus de ma compréhension actuelle et je vais me contenter d’expliquer dans les grandes lignes ce qui à mon niveau est important.
L-value
Il est plus facile de penser à une L-value (également appelé “locator value”) comme à une fonction ou un objet (ou une expression qui s’évalue comme une fonction ou un objet). Toutes les L-values se voient assigné une adresse en mémoire.
Initialement dans les vielles version de C++, les L-values étaient définies comme “valeurs qui peuvent se retrouver à gauche de l’opérateur d’assignation”. Celà dit, depuis l’introduction du mot clé const
, les l-values se sont fait diviser en deux sous-catégories:
- L_value modifiables
- L_value non-modifiables
R-value
Il est plus simple de penser à une R-value comme étant tout ce qui n’est pas une L-value. Celà inclue:
- “literals” (ex.
5
) - valeurs temporaires (ex.
x+1
) - objets anonymes (ex.
Fraction(5,2)
)
Les r-values sont typiquement:
- évaluées pour leur valeurs
- ont une portée
- on ne peut pas leur assigner quelque chose
Cette règle de non assignement possible
référence à une L-value
On ne peut référencer une L-value qu’avec des L-value modifiables:
reference L-value | peut être init avec | peut modifier |
---|---|---|
L-value modifiable | oui | oui |
L-value non-modifiable | non | non |
R-value | non | non |
Les références L-value à des objets const
peuvent être initialisées indiférament avec des L_values ou des R-values cependant ces valeurs ne peuvent être modifiés:
reference L-value à une const |
peut être init avec | peut modifier |
---|---|---|
L-value modifiable | oui | non |
L-value non-modifiable | oui | non |
R-value | oui | non |
Les références sur des objets
const
sont particulièrement utiles car elles nous permettent de passer n’importe quel type d’argument (L-value ou R-value) dans une fonction sans faire de copie de l’argument.
référence à une R-value
Une référence R-value est une référence faite pour être uniquement initialisée avec une R-value.
Une référence sur une L-value est faite avec une esperluette
&
, une référence sur une R-value se fait avec 2 esperluettes&&
.
int x = 5;
int &lref = x; // référence L-value initialisée avec L-value x
int &&rref = 5; // référence R-value initialisée avec R-value 5
Les références R-value ne peuvent être initialisées avec des L-value:
reference R-value | peut être init avec | peut modifier |
---|---|---|
L-value modifiable | non | non |
L-value non-modifiable | non | non |
R-value | oui | oui |
reference R-value à une const |
peut être init avec | peut modifier |
---|---|---|
L-value modifiable | non | non |
L-value non-modifiable | non | non |
R-value | oui | non |
Les références R-value ont deux propriétés qui nous interesse pour le moment:
- Elles étendent la durée de vie (lifespan) de l’objet qui les initialise avec la durée de vie (lifespan) de la référence R-value.
Les références L_value à des objets
const
fait la même chose mais c’est bien plus utilie pour les références R-value à cause de la portée des expressions des R-value. - Les références R-value non
cont
nous permettent de modifier la R-value.
Quelles lignes ne vont pas compiler?
int main() {
int x;
// l-value references
int &ref1 = x; // A
int &ref2 = 5; // B
const int &ref3 = x; // C
const int &ref4 = 5; // D
// r-value references
int &&ref5 = x; // E
int &&ref6 = 5; // F
const int &&ref7 = x; // G
const int &&ref8 = 5; // H
return 0;
}
Si nous suivons les indications des tableaux précédent on peut identfier que les cas: B
, E
et G
ne compileront pas.
Exemples:
#include<iostream>
using namespace std;
class Fraction
{
private:
int _num;
int _den;
public:
Fraction(int num=0, int den=1): _num(num), _den(den) {}
friend ostream &operator<<(ostream &out, const Fraction &f1) {
out << f1._num << "/" << f1._den;
return out;
}
};
int main() {
Fraction &&rref = Fraction(3,5); // reference R-value à une fraction anonyme
cout << rref << "\n";
return 0;
// fin de portée de rref et Fraction anonyme
}
// output:
// 3/5
En tant qu’objet anonyme, Fraction(3,5) devrait normalement se retrouver hors contexte à la fin de l’expression dans laquelle elle est définie mais comme on la référence par une R-value, sa durée de vie est étendue jusqu’à la fin du bloc.
Voyons maintenant un exemple moins intuitif:
int main() {
int &&rref = 5; // Comme nous initialisons une référence R-value à une expression literale
// un "temporaire" avec une valeur de 5 est créé ici.
rref = 10;
cout << rref << "\n";
return 0;
}
// output:
// 10
Il peut sembler étrange d’initialiser une référence R-value avec une expression literale et ensuite pouvoir changer cette valeur. Ce qu’il se passe quand on initialise une R-value avec une expression litérale, c’est qu’un temporaire est créé à partir de l’expression litérale pour que la référence puisse référencer un objet (temporaire) et non une expression litérale.
Les références R-value ne sont pas souvent utilisées comme dans ces deux exemples.
référence R-value comme paramètre de fonction
Utilité principale des références R-value. Très utile pour la surcharge de fonctions quand on veut avoir des comportements différents pour les arguments L-value et R-value.
void func(const int &lref) { // argument L-value choisira cette fonction
cout << "reference L-value a const\n";
}
void func(int &&lref) { // argument R-value choisira cette fonction
cout << "reference R-value\n";
}
int main() {
int x = 5;
func(x); // argument L-value appel la version L-value de func()
func(5); // argument R-value appel la version R-value de func()
return 0;
}
// output:
// reference L-value a const
// reference R-value
Pourquoi ?
Ces mécanismes sont importants pour pouvoir comprendre la sémantique de déplacement
Important
On ne devrait presque jamais retourner une référence sur une R-value pour la même raison qu’on ne devrait presque jamais retourner une référence L-value! Si on le fait, dans la majorité des cas, la valeur de retour sera une référence pointant sur un objet qui est sorti de son contexte à la fin de la fonction!