Partage via


Déclarateur de référence rvalue : &&

Contient une référence à une expression rvalue.

Syntaxe

rvalue-reference-type-id:
type-specifier-seq && attribute-specifier-seqopt ptr-abstract-declaratoropt

Notes

Les références rvalue vous permettent de différencier une lvalue d'une rvalue. Les références lvalue et rvalue sont syntaxiquement et sémantiquement similaires, mais elles suivent des règles légèrement différentes. Pour plus d’informations sur les lvalues et les rvalues, consultez Lvalues et rvalues. Pour plus d’informations sur les références lvalue, consultez Déclarateur de référence lvalue : &.

Les sections suivantes décrivent comment les références rvalue prennent en charge l’implémentation de la sémantique de mouvement et du transfert parfait.

Sémantique de mouvement

Les références rvalue prennent en charge l’implémentation de la sémantique de mouvement, qui peut augmenter considérablement les performances de vos applications. La sémantique de déplacement vous permet d'écrire du code qui transfère des ressources (telles que la mémoire allouée de manière dynamique) d'un objet vers un autre. La sémantique de mouvement fonctionne car elle permet de transférer des ressources depuis des objets temporaires, c’est-à-dire qui ne peuvent pas être référencés ailleurs dans le programme.

Pour implémenter la sémantique de mouvement, vous fournissez en général à votre classe un constructeur de mouvement et éventuellement un opérateur d’affectation de mouvement (operator=). Les opérations de copie et d'assignation dont les sources sont des rvalues profitent ensuite automatiquement de la sémantique de déplacement. Contrairement au constructeur de copie par défaut, le compilateur ne fournit pas de constructeur de mouvement par défaut. Pour plus d’informations sur l’écriture et l’utilisation d’un constructeur de mouvement, consultez Constructeurs de mouvement et opérateurs d’affectation de mouvement.

Vous pouvez aussi surcharger des fonctions et des opérateurs ordinaires pour tirer parti de la sémantique de déplacement. Visual Studio 2010 introduit la sémantique de mouvement dans la bibliothèque C++ standard. Par exemple, la classe string implémente des opérations qui utilisent la sémantique de mouvement. Prenons l'exemple suivant qui concatène plusieurs chaînes et affiche le résultat :

// string_concatenation.cpp
// compile with: /EHsc
#include <iostream>
#include <string>
using namespace std;

int main()
{
   string s = string("h") + "e" + "ll" + "o";
   cout << s << endl;
}

Avant Visual Studio 2010, chaque appel à operator+ alloue et retourne un nouvel objet string temporaire (une rvalue). operator+ ne peut pas ajouter une chaîne à l’autre, car il ne sait pas si les chaînes sources sont des lvalues ou des rvalues. Si les chaînes sources sont toutes deux des lvalues, elles peuvent être référencées ailleurs dans le programme et ne doivent donc pas être modifiées. Vous pouvez modifier operator+ pour qu’il prenne des valeurs rvalues en utilisant des références rvalue, qui ne peuvent pas être référencées ailleurs dans le programme. Avec cette modification, operator+ peut maintenant ajouter une chaîne à une autre. La modification réduit considérablement le nombre d’allocations de mémoire dynamiques que la classe string doit effectuer. Pour plus d’informations sur la classe string, consultez Classe basic_string.

La sémantique de mouvement est également utile quand le compilateur ne peut pas utiliser l’optimisation de la valeur de retour (RVO) ou l’optimisation de la valeur de retour nommée (NRVO). Dans ces cas-là, le compilateur appelle le constructeur de déplacement si le type le définit.

Pour mieux comprendre la sémantique de déplacement, prenez comme exemple l'insertion d'un élément dans un objet vector. Si la capacité de l’objet vector est dépassée, l’objet vector doit réallouer suffisamment de mémoire pour ses éléments, puis copier chaque élément vers un autre emplacement de mémoire afin de libérer de l’espace pour l’élément inséré. Quand une opération d’insertion copie un élément, elle crée d’abord un nouvel élément. Ensuite, elle appelle le constructeur de copie pour copier les données de l’élément précédent vers le nouvel élément. Enfin, elle détruit l’élément précédent. La sémantique de mouvement vous permet de déplacer directement des objets sans qu’il soit nécessaire d’exécuter des opérations d’allocation mémoire et de copie coûteuses en ressources.

Pour tirer parti de la sémantique de déplacement dans l'exemple vector, vous pouvez écrire un constructeur de déplacement pour déplacer des données d'un objet vers un autre.

Pour plus d’informations sur l’introduction de la sémantique de mouvement dans la bibliothèque standard C++ de Visual Studio 2010, consultez Bibliothèque standard C++.

Transfert parfait

Le transfert parfait réduit le besoin en fonctions surchargées et permet d'éviter le problème de transfert. Le problème de transfert peut se produire quand vous écrivez une fonction générique qui prend des références comme paramètres. Si elle passe (ou transfère) ces paramètres à une autre fonction, par exemple si elle prend un paramètre de type const T&, la fonction appelée ne peut pas modifier la valeur de ce paramètre. Si la fonction générique prend un paramètre de type T&, la fonction ne peut pas être appelée en utilisant une rvalue (comme un objet temporaire ou un littéral d’entier).

Normalement, pour résoudre ce problème, vous devez fournir des versions surchargées de la fonction générique qui acceptent T& et const T& pour chacun de ses paramètres. Par conséquent, le nombre de fonctions surchargées augmente de façon exponentielle avec le nombre de paramètres. Les références rvalue vous permettent d’écrire une version d’une fonction qui accepte des arguments arbitraires. Ensuite, cette fonction peut les transférer à une autre fonction comme si l’autre fonction avait été appelée directement.

Prenez l'exemple suivant qui déclare quatre types, W, X, Y et Z. Le constructeur pour chaque type prend une combinaison différente de références lvalue const et non-const comme paramètres.

struct W
{
   W(int&, int&) {}
};

struct X
{
   X(const int&, int&) {}
};

struct Y
{
   Y(int&, const int&) {}
};

struct Z
{
   Z(const int&, const int&) {}
};

Supposez que vous souhaitez écrire une fonction générique qui génère des objets. L'exemple suivant montre une manière d'écrire cette fonction :

template <typename T, typename A1, typename A2>
T* factory(A1& a1, A2& a2)
{
   return new T(a1, a2);
}

L'exemple suivant montre un appel valide à la fonction factory :

int a = 4, b = 5;
W* pw = factory<W>(a, b);

Cependant, l’exemple suivant ne contient pas un appel valide à la fonction factory. La raison en est que factory prend des références lvalue qui sont modifiables en tant que paramètres, alors qu’elle est appelée en utilisant des rvalues :

Z* pz = factory<Z>(2, 2);

Normalement, pour résoudre ce problème, vous devez créer une version surchargée de la fonction factory pour chaque combinaison de paramètres A& et const A&. Les références rvalue vous permettent d'écrire une version de la fonction factory, comme indiqué dans l'exemple suivant :

template <typename T, typename A1, typename A2>
T* factory(A1&& a1, A2&& a2)
{
   return new T(std::forward<A1>(a1), std::forward<A2>(a2));
}

Cet exemple utilise des références rvalue comme paramètres de la fonction factory. L’objectif de la fonction std::forward est de transférer les paramètres de la fonction factory au constructeur de la classe de modèle.

L'exemple suivant illustre la fonction main qui utilise la fonction modifiée factory pour créer des instances des classes W, X, Y et Z. La fonction factory modifiée transfère ses paramètres (lvalues ou rvalues) au constructeur de classe approprié.

int main()
{
   int a = 4, b = 5;
   W* pw = factory<W>(a, b);
   X* px = factory<X>(2, b);
   Y* py = factory<Y>(a, 2);
   Z* pz = factory<Z>(2, 2);

   delete pw;
   delete px;
   delete py;
   delete pz;
}

Propriétés des références rvalue

Vous pouvez surcharger une fonction pour qu’elle prenne une référence lvalue et une référence rvalue.

En surchargeant une fonction pour qu’elle prenne une référence lvalue ou une référence rvalue const, vous pouvez écrire du code qui fait la distinction entre des objets non modifiables (lvalues) et des valeurs temporaires modifiables (rvalues). Vous pouvez passer un objet à une fonction qui prend une référence rvalue sauf si l’objet est marqué comme const. L'exemple suivant illustre la fonction f, qui est surchargée pour accepter une référence lvalue et une référence rvalue. La fonction main appelle f avec des lvalues et une rvalue.

// reference-overload.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;

// A class that contains a memory resource.
class MemoryBlock
{
   // TODO: Add resources for the class here.
};

void f(const MemoryBlock&)
{
   cout << "In f(const MemoryBlock&). This version can't modify the parameter." << endl;
}

void f(MemoryBlock&&)
{
   cout << "In f(MemoryBlock&&). This version can modify the parameter." << endl;
}

int main()
{
   MemoryBlock block;
   f(block);
   f(MemoryBlock());
}

Cet exemple produit la sortie suivante :

In f(const MemoryBlock&). This version can't modify the parameter.
In f(MemoryBlock&&). This version can modify the parameter.

Dans cet exemple, le premier appel à f passe une variable locale (une lvalue) comme argument. Le deuxième appel à f passe un objet temporaire comme argument. Comme l’objet temporaire ne peut pas être référencé ailleurs dans le programme, l’appel crée une liaison à la version surchargée de f qui prend une référence rvalue, qui est libre de modifier l’objet.

Le compilateur traite une référence rvalue nommée en tant que lvalue et une référence rvalue non nommée en tant que rvalue.

Les fonctions qui prennent une référence rvalue comme paramètre traitent le paramètre en tant que lvalue dans le corps de la fonction. Le compilateur traite une référence rvalue nommée comme une lvalue. La raison en est qu’un objet nommé peut être référencé par plusieurs parties d’un programme. Il est dangereux de permettre à plusieurs parties d’un programme de modifier ou de supprimer des ressources de cet objet. Par exemple, si plusieurs parties d’un programme tentent de transférer des ressources à partir du même objet, seul le premier transfert réussit.

L'exemple suivant illustre la fonction g, qui est surchargée pour accepter une référence lvalue et une référence rvalue. La fonction f accepte une référence rvalue comme paramètre (une référence rvalue nommée) et retourne une référence rvalue (une référence rvalue sans nom). Dans l'appel à g à partir de f, la résolution de surcharge sélectionne la version de g qui accepte une référence lvalue, car le corps de f traite son paramètre comme une lvalue. Dans l’appel à g à partir de main, la résolution de surcharge sélectionne la version de g qui accepte une référence rvalue car f retourne une référence rvalue.

// named-reference.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;

// A class that contains a memory resource.
class MemoryBlock
{
   // TODO: Add resources for the class here.
};

void g(const MemoryBlock&)
{
   cout << "In g(const MemoryBlock&)." << endl;
}

void g(MemoryBlock&&)
{
   cout << "In g(MemoryBlock&&)." << endl;
}

MemoryBlock&& f(MemoryBlock&& block)
{
   g(block);
   return move(block);
}

int main()
{
   g(f(MemoryBlock()));
}

Cet exemple produit la sortie suivante :

In g(const MemoryBlock&).
In g(MemoryBlock&&).

Dans l’exemple, la fonction main passe une rvalue à f. Le corps de f traite son paramètre nommé comme une lvalue. L'appel de f à g lie le paramètre à une référence lvalue (première version surchargée de g).

  • Vous pouvez caster une lvalue en une référence rvalue.

La fonction std::move de la bibliothèque standard C++ vous permet de convertir un objet en une référence rvalue à cet objet. Vous pouvez aussi utiliser le mot clé static_cast pour caster une lvalue en une référence rvalue, comme illustré dans l’exemple suivant :

// cast-reference.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;

// A class that contains a memory resource.
class MemoryBlock
{
   // TODO: Add resources for the class here.
};

void g(const MemoryBlock&)
{
   cout << "In g(const MemoryBlock&)." << endl;
}

void g(MemoryBlock&&)
{
   cout << "In g(MemoryBlock&&)." << endl;
}

int main()
{
   MemoryBlock block;
   g(block);
   g(static_cast<MemoryBlock&&>(block));
}

Cet exemple produit la sortie suivante :

In g(const MemoryBlock&).
In g(MemoryBlock&&).

Les modèles de fonction déduisent leurs types d’argument de modèle, puis utilisent des règles de réduction de référence.

Un modèle de fonction qui passe (ou transfère) ses paramètres à une autre fonction est un modèle courant. Il est important de comprendre comment la déduction de type de modèle fonctionne pour les modèles de fonction qui prennent des références rvalue.

Si l'argument de fonction est une rvalue, le compilateur déduit l'argument comme étant une référence rvalue. Par exemple, supposons que vous passez une référence rvalue à un objet de type X à une fonction de modèle qui prend le type T&& comme paramètre. La déduction d’argument de modèle déduit que T est X : le paramètre a donc le type X&&. Si l’argument de fonction est une lvalue ou une lvalue const, le compilateur déduit que son type est une référence lvalue ou une référence lvalue const de ce type.

L'exemple suivant déclare un modèle de structure, puis le spécialise pour différents types de références. La fonction print_type_and_value accepte une référence rvalue comme paramètre et la transfère à la version spécialisée appropriée de la méthode S::print. La fonction main illustre les différentes façons d'appeler la méthode S::print.

// template-type-deduction.cpp
// Compile with: /EHsc
#include <iostream>
#include <string>
using namespace std;

template<typename T> struct S;

// The following structures specialize S by
// lvalue reference (T&), const lvalue reference (const T&),
// rvalue reference (T&&), and const rvalue reference (const T&&).
// Each structure provides a print method that prints the type of
// the structure and its parameter.

template<typename T> struct S<T&> {
   static void print(T& t)
   {
      cout << "print<T&>: " << t << endl;
   }
};

template<typename T> struct S<const T&> {
   static void print(const T& t)
   {
      cout << "print<const T&>: " << t << endl;
   }
};

template<typename T> struct S<T&&> {
   static void print(T&& t)
   {
      cout << "print<T&&>: " << t << endl;
   }
};

template<typename T> struct S<const T&&> {
   static void print(const T&& t)
   {
      cout << "print<const T&&>: " << t << endl;
   }
};

// This function forwards its parameter to a specialized
// version of the S type.
template <typename T> void print_type_and_value(T&& t)
{
   S<T&&>::print(std::forward<T>(t));
}

// This function returns the constant string "fourth".
const string fourth() { return string("fourth"); }

int main()
{
   // The following call resolves to:
   // print_type_and_value<string&>(string& && t)
   // Which collapses to:
   // print_type_and_value<string&>(string& t)
   string s1("first");
   print_type_and_value(s1);

   // The following call resolves to:
   // print_type_and_value<const string&>(const string& && t)
   // Which collapses to:
   // print_type_and_value<const string&>(const string& t)
   const string s2("second");
   print_type_and_value(s2);

   // The following call resolves to:
   // print_type_and_value<string&&>(string&& t)
   print_type_and_value(string("third"));

   // The following call resolves to:
   // print_type_and_value<const string&&>(const string&& t)
   print_type_and_value(fourth());
}

Cet exemple produit la sortie suivante :

print<T&>: first
print<const T&>: second
print<T&&>: third
print<const T&&>: fourth

Pour résoudre chaque appel à la fonction print_type_and_value, le compilateur effectue d’abord une déduction d’argument de modèle. Le compilateur applique ensuite des règles de réduction de référence quand il remplace les types des paramètres par les arguments de modèle déduits. Par exemple, le passage de la variable locale s1 à la fonction print_type_and_value provoque la production par le compilateur de la signature de fonction suivante :

print_type_and_value<string&>(string& && t)

Le compilateur utilise des règles de réduction de référence pour réduire la signature :

print_type_and_value<string&>(string& t)

Cette version de la fonction print_type_and_value transfère ensuite son paramètre à la version spécialisée appropriée de la méthode S::print.

Le tableau suivant résume les règles de réduction de référence pour la déduction du type d'argument template :

Type développé Type réduit
T& & T&
T& && T&
T&& & T&
T&& && T&&

La déduction d’argument template est un élément important de l’implémentation du transfert parfait. La section Transfert parfait décrit plus en détail le transfert parfait.

Résumé

Les références rvalue différencient les lvalues des rvalues. Pour améliorer les performances de vos applications, elles peuvent éliminer les allocations de mémoire et les opérations de copie inutiles. Elles vous permettent aussi d’écrire une fonction qui accepte des arguments arbitraires. Cette fonction peut les transférer à une autre fonction comme si l’autre fonction avait été appelée directement.

Voir aussi

Expressions avec des opérateurs unaires
Déclarateur de référence lvalue : &
Lvalues et rvalues
Constructeurs de mouvement et opérateurs d’affectation de mouvement (C++)
Bibliothèque C++ standard