Chapitre 2
REFERENCES RVALUE
ET MOUVEMENTS
 
 
Précédent Suivant
 

Références rvalue
et mouvements
Rappel sur les rvalues (1/2)
* En C++03, deux catégories de valeurs
* A noter que ces notions évoluent avec les normes du C++
* lvalue
* Historiquement: valeur à gauche (left-handed) d'une affectation
* Valeur "localisable": accessible via variable, référence ou pointeur
* Zone mémoire identifiable ==> peut être modifiée
* rvalue
* Historiquement: valeur à droite (right-handed) d'une affectation
* Valeur ne pouvant pas être modifiée
* Typiquement, une valeur à usage ponctuel
* Littéral: f(5);
* Temporaire construit à la volée: f(string("Hello !"));
* Retour (par copie) d'une fonction: f(a+b);
Rappel sur les rvalues (2/2)
* En C++03, référence non constante sur une rvalue interdite
* Exemple
* f(string("Hello !"));
* Création à la volée d'un objet ==> rvalue
* Quel prototype pour récupérer la rvalue ?
* Copie: void f(string s); ==> OK
* Référence constante: void f(const string & s); ==> OK
* Référence non constante: void f(string & s); ==> non !
Nouvelle sémantique (1/2)
* En C++11, changement de définition
* Et nouvelles catégories de valeurs: xvalue, prvalue...
* http://en.cppreference.com/w/cpp/language/value_category
* De manière informelle
* lvalue ==> comme avant
* rvalue ==> valeur qui peut être modifiée sans effet de bord
* Cas d'un temporaire
* Usage unique, donc sa modification est sans conséquence
* Ce qui change concrètement
* On peut faire une référence non constante sur une rvalue
* On peut volontairement transformer une lvalue en rvalue
Nouvelle sémantique (2/2)
* Nouvelle syntaxe: &&
* Référence sur une rvalue
* Il s'agit d'une référence ==> mêmes règles que "&"
* Caractère constant / non constant
* Une méthode ne peut pas retourner de référence sur une variable locale
* Retour à l'exemple précédent
* Rappel: f(string("Hello !"));
* Référence sur rvalue non constante: void f(string && s); ==> OK
* Mais cette version de la fonction n'est utilisable que pour une rvalue
* Pourquoi avoir une référence non constante sur une rvalue ?
* Pour pouvoir la "dépouiller"
* Autrement dit, récupérer son contenu directement au lieu de le copier
* La rvalue ne sera plus utilisable par la suite
Dépouillement d'objet (1/3)
* Exemple
class Vecteur {
private:
int * tab_;
unsigned taille_;
public:
explicit Vecteur(unsigned);
Vecteur(const Vecteur &);
~Vecteur(void);
Vecteur & operator = (const Vecteur &);
...
};
Dépouillement d'objet (2/3)
* Dépouiller un vecteur
void Vecteur::depouiller(Vecteur && victime) {
if (tab_) delete [] tab_;
tab_ = victime.tab_;
taille_ = victime.taille_;
victime.tab_ = 0;
victime.taille_ = 0;
}
* Utilisation
* Vecteur v1 = ...;
* Vecteur v2 = ...;
* v1.dépouiller(v2) ==> interdit ("v2" n'est pas une rvalue)
* Vecteur creerVecteur(void);
* v1.dépouiller(creerVecteur()) ==> OK
Dépouillement d'objet (3/3)
* Dépouillement ==> l'objet n'est plus utilisable...
* ...sauf qu'il doit être détruit !
==> Conserver une certaine cohérence des objets dépouillés
* Pour que l'appel au destructeur libère bien les ressources restantes
* Autre possibilité: échanger les contenus
void Vecteur::depouiller(Vecteur && victime) {
std::swap(tab_,victime.tab_);
std::swap(taille_,victime.taille_);
}
* Le dépouillement peut s'avérer utile pour optimiser la copie d'objets
Opérateurs de mouvement (1/3)
* Exemple: v3 = v1+v2;
* Opérateur "+"
==> Construction variable locale
==> Retour variable locale par copie
* Affectation
==> Copie du retour
* Pire des cas (sans optimisation) ==> 2 copies inutiles du tableau
* Construction par copie + affectation
* Remarque: l'optimisation devrait éviter la construction par copie du retour
* C++11 introduit 2 nouveaux opérateurs pour optimiser la copie d'objets
* Constructeur de mouvement / move constructor
* Vecteur(Vecteur && v)
* Affectation de mouvement / move assignment
* Vecteur & operator = (Vecteur && v)
Opérateurs de mouvement (2/3)
* Constructeur de mouvement
Vecteur(Vecteur && v)
: tab_(v.tab_), taille_(v.taille_) {
v.tab_ = 0;
v.taille_ = 0;
}
* Affectation de mouvement
Vecteur & operator = (Vecteur && v) {
std::swap(tab_,v.tab_);
std::swap(taille_,v.taille_);
return *this;
}
* Remarque: référence constante sur rvalue sans intérêt
Opérateurs de mouvement (3/3)
* Sélection automatique de l'opérateur le mieux adapté
* Pas d'opérateur de mouvement ==> opérateur de copie
* Opérateurs de mouvement + copie disponibles
* Argument = lvalue ou rvalue constante ==> opérateur de copie
* Argument = rvalue non constante ==> opérateur de mouvement
* Quand définir ces opérateurs de mouvement ?
* Lorsque la copie est coûteuse
* La STL utilisera ces opérateurs autant que possible
* Sous certaines conditions, opérateurs disponibles par défaut
* http://en.cppreference.com/w/cpp/language/move_constructor
* http://en.cppreference.com/w/cpp/language/move_operator
Conversion en rvalue (1/2)
* Comment "forcer" l'utilisation de ces opérateurs ?
* Possibilité de convertir une lvalue en rvalue ==> std::move
* Cela permet de favoriser un mouvement plutôt qu'une copie
* Mais ensuite l'objet concerné ne doit plus être utilisé
* Exemple
template <typename T> inline void swap(T & a,T & b) {
T tmp = a;
a = b;
b = tmp;
}
* Trop de copies !
* Après chaque affectation, la valeur du membre de droite sans intérêt
* On pourrait donc le dépouiller plutôt que le copier
* En utilisant les opérateurs de mouvement
Conversion en rvalue (2/2)
* Solution potentiellement plus efficace
template <typename T> inline void swap(T & a,T & b) {
T tmp = std::move(a);
a = std::move(b);
b = std::move(tmp);
}
* Plus aucune copie, mais des mouvements...
* ...à condition que "T" implémente les opérateurs de mouvement
* Comment fonctionne "std::move" ?
* Conversion via "static_cast": T & ? T &&
* Mais il y a quelques subtilités (expliquées plus tard)
* Ne forcer la conversion que si la valeur devient inutile !
Collapsing rules (1/3)
* "&&" et les templates ==> argh !!!
* int && ==> rvalue
* T && ==> rvalue ou lvalue !
* Nouveauté C++11: les "collapsing rules"
* Règles pour gérer les "références de références"
* Permettent de ramener les types à leur plus simple expression
* Voilà les règles utilisées par le compilateur
* T & + & ==> T &
* T & + && ==> T &
* T && + & ==> T &
* T && + && ==> T &&
* Impact sur la déduction de type à l'instanciation d'un template
* Objectif: faciliter le traitement générique d'arguments
* Par exemple, pour répondre au problème du "perfect forwarding"
Collapsing rules (2/3)
* Comment capter le caractère constant (ou non) d'un argument ?
* template <typename T> void f(T & x);
* N'accepte que les lvalues non constantes
* Alors qu'on cherche à capter les valeurs constantes et non constantes
* template <typename T> void f(const T & x);
* Accepte les lvalues et les rvalues
* Mais on force le caractère constant ==> perte d'information sur le type
* template <typename T> void f(T x);
* Accepte les lvalues et les rvalues
* Mais c'est un passage par copie ==> perte d'information sur le type
* Aucune solution idéale en C++03
Collapsing rules (3/3)
* Solution C++11: template <typename T> void f(T && x);
* Application des collapsing rules
* int x;
* const int y;
* f(5) ==> rvalue ==> f<int>(int &&)
* f(x) ==> lvalue ==> f<int &>(int &)
* f(y) ==> lvalue ==> f<const int &>(const int &)
* Avec les templates, "&&" ne signifie donc pas toujours rvalue
* T && = "référence universelle" (Scott Meyers)
* Référence lvalue ou rvalue, constante ou non
Perfect forwarding (1/2)
* Une fois passée en argument, une rvalue devient lvalue
* template <typename T> void f(T && x) { g(x); }
* template <typename T> void g(T &&);
* int x;
* const int y;
* f(5) ==> g<int &>(int &) ==> caractère rvalue perdu
* f(x) ==> g<int &>(int &)
* f(y) ==> g<const int &>(const int &)
* "Perfect forwarding" ==> transfert à l'identique du type
* Conservation des caractères
* Constant / non constant
* rvalue / lvalue
* Impossible en C++03
Perfect forwarding (2/2)
* T && ==> caractère constant / non constant contenu dans "T"
* Contrairement aux références classiques "T &"
* std::forward ==> conservation du caractère rvalue / lvalue
* template <typename T> void f(T && x)
{ g(std::forward<T>(x)); }
* template <typename T> void g(T &&);
* int x;
* const int y;
* f(5) ==> g<int>(int &&)
* f(x) ==> g<int &>(int &)
* f(y) ==> g<const int &>(const int &)
Implémentation de "std::move"
* Que se cache-t-il derrière "std::move" et "std::forward" ?
* Implémentation possible de "std::move"
* template <typename T> using remove_ref
= typename std::remove_reference<T>::type;
* template <typename T>
inline remove_ref<T> && move(T && x) {
return static_cast<remove_ref<T> &&>(x);
}
* std::remove_reference ==> perte du caractère référence du type
* remove_ref<T> = alias pour simplifier l'écriture
* Retirer la référence est nécessaire à cause des collapsing rules
* T & + && ==> T &
* remove_ref<T &> + && ==> T &&
Implémentation de "std::forward"
* Implémentation possible de "std::forward"
template <typename T>
inline T && forward(remove_ref<T> & x) {
return static_cast<T &&>(x);
}
* Perfect forwarding garanti dans un contexte précis
* "T" doit avoir été déduit d'une référence universelle
* Et seul "T" doit servir à l'instanciation du template
* Contexte type
* template <typename T>
void f(T && x) { g(std::forward<T>(x)); }
* Explication
* f(5) ==> rvalue ==> T = int ==> static_cast<int &&>
* f(x) ==> lvalue ==> T = int & ==> static_cast<int &>

Voici le code source des exemples présentés:

  • cpp11_move.cpp
    Illustre les possibilités offertes par les références rvalue et les opérateurs de mouvement.

  • cpp11_typeinfo.hpp
    Facilite la lecture des informations de type pour g++.