Que signifie T&& (double esperluette) en C ++ 11?

801

J'ai étudié certaines des nouvelles fonctionnalités de C ++ 11 et j'ai remarqué une double esperluette dans la déclaration de variables, comme T&& var.

Pour commencer, comment s'appelle cette bête? Je souhaite que Google nous permette de rechercher des signes de ponctuation comme celui-ci.

Qu'est-ce que cela signifie exactement ?

À première vue, cela semble être une double référence (comme les pointeurs doubles de style C T** var), mais j'ai du mal à penser à un cas d'utilisation pour cela.

paxdiablo
la source
55
J'ai ajouté cela à la FAQ c ++, car je suis sûr que cela reviendra plus à l'avenir.
GManNickG
3
question connexe sur la sémantique des mouvements
fredoverflow
41
Vous pouvez rechercher cela à l'aide de Google, il vous suffit de mettre votre phrase entre guillemets: google.com/#q="T%26%26 "a maintenant votre question comme premier coup. :)
sbi
Il y a une très bonne réponse facile à comprendre à une question similaire ici stackoverflow.com/questions/7153991/…
Daniel
2
J'ai reçu trois questions de stackoverflow en haut de la recherche dans Google pour "c ++ two esperluands parameter" et la vôtre était la première. Vous n'avez donc même pas besoin d'utiliser la ponctuation pour cela si vous pouvez épeler "deux paramètres esperluette".
sergiol

Réponses:

668

Il déclare une référence de valeur (doc de proposition de normes).

Voici une introduction aux références rvalue .

Voici un aperçu approfondi fantastique des références rvalue par l'un des développeurs de bibliothèques standard de Microsoft .

ATTENTION: l'article lié sur MSDN («Références Rvalue: fonctionnalités C ++ 0x dans VC10, partie 2») est une introduction très claire aux références Rvalue, mais fait des déclarations sur les références Rvalue qui étaient autrefois vraies dans le projet C ++ 11 standard, mais ne sont pas vraies pour le dernier! Plus précisément, il indique à divers points que les références rvalue peuvent se lier à lvalues, ce qui était une fois vrai, mais a été modifié (par exemple, int x; int && rrx = x; ne compile plus dans GCC) - drewbarbs 13 juillet 14 à 16:12

La plus grande différence entre une référence C ++ 03 (maintenant appelée référence lvalue en C ++ 11) est qu'elle peut se lier à une rvalue comme un temporaire sans avoir à être const. Ainsi, cette syntaxe est désormais légale:

T&& r = T();

Les références rvalue prévoient principalement les éléments suivants:

Déplacer la sémantique . Il est désormais possible de définir un constructeur de déplacement et un opérateur d'affectation de déplacement qui prennent une référence rvalue au lieu de la référence const-lvalue habituelle. Un mouvement fonctionne comme une copie, sauf qu'il n'est pas obligé de garder la source inchangée; en fait, il modifie généralement la source de sorte qu'il ne possède plus les ressources déplacées. Ceci est idéal pour éliminer les copies superflues, en particulier dans les implémentations de bibliothèque standard.

Par exemple, un constructeur de copie pourrait ressembler à ceci:

foo(foo const& other)
{
    this->length = other.length;
    this->ptr = new int[other.length];
    copy(other.ptr, other.ptr + other.length, this->ptr);
}

Si ce constructeur passait un temporaire, la copie serait inutile parce que nous savons que le temporaire sera juste détruit; pourquoi ne pas utiliser les ressources temporaires déjà allouées? En C ++ 03, il n'y a aucun moyen d'empêcher la copie car nous ne pouvons pas déterminer que nous avons passé un temporaire. En C ++ 11, nous pouvons surcharger un constructeur de mouvement:

foo(foo&& other)
{
   this->length = other.length;
   this->ptr = other.ptr;
   other.length = 0;
   other.ptr = nullptr;
}

Notez la grande différence ici: le constructeur de déplacement modifie en fait son argument. Cela "déplacerait" effectivement le temporaire dans l'objet en cours de construction, éliminant ainsi la copie inutile.

Le constructeur de déplacement serait utilisé pour les références temporaires et pour les références lvalue non const qui sont explicitement converties en références rvalue à l'aide de la std::movefonction (il effectue simplement la conversion). Le code suivant appelle à la fois le constructeur de déplacement pour f1et f2:

foo f1((foo())); // Move a temporary into f1; temporary becomes "empty"
foo f2 = std::move(f1); // Move f1 into f2; f1 is now "empty"

Transfert parfait . Les références rvalue nous permettent de transmettre correctement les arguments des fonctions de modèle. Prenons par exemple cette fonction d'usine:

template <typename T, typename A1>
std::unique_ptr<T> factory(A1& a1)
{
    return std::unique_ptr<T>(new T(a1));
}

Si nous appelons factory<foo>(5), l'argument sera déduit comme étant int&, qui ne sera pas lié à un littéral 5, même si foole constructeur de prend un int. Eh bien, nous pourrions utiliser à la place A1 const&, mais que faire si fooprend l'argument constructeur par référence non const? Pour créer une fonction d'usine vraiment générique, il faudrait surcharger l'usine encore A1&et encore A1 const&. Cela pourrait être bien si l'usine prend 1 type de paramètre, mais chaque type de paramètre supplémentaire multiplierait la surcharge nécessaire définie par 2. C'est très rapidement impossible à maintenir.

Les références rvalue résolvent ce problème en permettant à la bibliothèque standard de définir une std::forwardfonction capable de transmettre correctement les références lvalue / rvalue. Pour plus d'informations sur le std::forwardfonctionnement, consultez cette excellente réponse .

Cela nous permet de définir la fonction d'usine comme ceci:

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

Désormais, l'argument rvalue / lvalue-ness est préservé lorsqu'il est passé au Tconstructeur de. Cela signifie que si factory est appelé avec une rvalue, Tle constructeur de est appelé avec une rvalue. Si factory est appelé avec une lvalue, Tle constructeur de est appelé avec une lvalue. La fonction d'usine améliorée fonctionne grâce à une règle spéciale:

Lorsque le type de paramètre de fonction est de la forme T&&Test un paramètre de modèle et que l'argument de fonction est une valeur de type l A, le type A&est utilisé pour la déduction d'argument de modèle.

Ainsi, nous pouvons utiliser l'usine comme ceci:

auto p1 = factory<foo>(foo()); // calls foo(foo&&)
auto p2 = factory<foo>(*p1);   // calls foo(foo const&)

Propriétés de référence rvalue importantes :

  • Pour la résolution de surcharge, lvalues ​​préfère la liaison aux références lvalue et rvalues ​​préfère la liaison aux références rvalue . D'où la raison pour laquelle les temporaires préfèrent invoquer un constructeur de déplacement / opérateur d'affectation de déplacement plutôt qu'un constructeur de copie / opérateur d'affectation.
  • Les références rvalue se lieront implicitement aux rvalues ​​et aux temporaires qui sont le résultat d'une conversion implicite . ie float f = 0f; int&& i = f;est bien formé car float est implicitement convertible en int; la référence serait à un temporaire résultant de la conversion.
  • Les références rvalue nommées sont des lvalues. Les références rvalue sans nom sont des rvalues. Ceci est important pour comprendre pourquoi l' std::moveappel est nécessaire dans:foo&& r = foo(); foo f = std::move(r);
Peter Huene
la source
65
+1 pour Named rvalue references are lvalues. Unnamed rvalue references are rvalues.; sans le savoir, j'ai du mal à comprendre pourquoi les gens font T &&t; std::move(t);depuis longtemps des ctors de mouvement, etc.
legends2k
@MaximYegorushkin: Dans cet exemple, r est lié à une valeur r pure (temporaire) et donc le temporaire devrait avoir sa durée de vie étendue, non?
Peter Huene
@PeterHuene Je reprends cela, une référence de valeur r prolonge la durée de vie d'un temporaire.
Maxim Egorushkin
32
ATTENTION : l'article lié sur MSDN («Références Rvalue: fonctionnalités C ++ 0x dans VC10, partie 2») est une introduction très claire aux références Rvalue, mais fait des déclarations sur les références Rvalue qui étaient autrefois vraies dans le projet C ++ 11 standard, mais ne sont pas vraies pour le dernier! Plus précisément, il indique à divers points que les références rvalue peuvent se lier à lvalues, ce qui était une fois vrai, mais a été modifié (par exemple, int x; int &&rrx = x; ne compile plus dans GCC)
drewbarbs
@PeterHuene Dans l'exemple ci-dessus, n'est-ce pas typename identity<T>::type& aéquivalent à T&?
ibp73
81

Il désigne une référence rvalue. Les références Rvalue ne se lient qu'aux objets temporaires, sauf indication contraire explicite. Ils sont utilisés pour rendre les objets beaucoup plus efficaces dans certaines circonstances et pour fournir une fonctionnalité connue sous le nom de transfert parfait, ce qui simplifie considérablement le code du modèle.

En C ++ 03, vous ne pouvez pas faire la distinction entre une copie d'une valeur l non mutable et d'une valeur r.

std::string s;
std::string another(s);           // calls std::string(const std::string&);
std::string more(std::string(s)); // calls std::string(const std::string&);

En C ++ 0x, ce n'est pas le cas.

std::string s;
std::string another(s);           // calls std::string(const std::string&);
std::string more(std::string(s)); // calls std::string(std::string&&);

Considérez l'implémentation derrière ces constructeurs. Dans le premier cas, la chaîne doit effectuer une copie pour conserver la sémantique des valeurs, ce qui implique une nouvelle allocation de tas. Cependant, dans le deuxième cas, nous savons à l'avance que l'objet qui a été transmis à notre constructeur doit immédiatement être détruit, et il n'a pas à rester intact. Nous pouvons échanger efficacement les pointeurs internes et ne pas effectuer de copie du tout dans ce scénario, qui est beaucoup plus efficace. La sémantique de déplacement profite à toute classe dont la copie coûteuse ou interdite des ressources référencées en interne. Considérons le cas de std::unique_ptr- maintenant que notre classe peut faire la distinction entre les temporels et les non-temporels, nous pouvons faire fonctionner la sémantique de mouvement correctement afin qu'elle unique_ptrne puisse pas être copiée mais déplacée, ce qui signifie questd::unique_ptrpeut être stocké légalement dans des conteneurs standard, trié, etc., contrairement à C ++ 03 std::auto_ptr.

Maintenant, nous considérons l'autre utilisation des références rvalue - un transfert parfait. Considérez la question de lier une référence à une référence.

std::string s;
std::string& ref = s;
(std::string&)& anotherref = ref; // usually expressed via template

Je ne me souviens pas de ce que C ++ 03 dit à ce sujet, mais en C ++ 0x, le type résultant lors du traitement des références rvalue est critique. Une référence rvalue à un type T, où T est un type de référence, devient une référence de type T.

(std::string&)&& ref // ref is std::string&
(const std::string&)&& ref // ref is const std::string&
(std::string&&)&& ref // ref is std::string&&
(const std::string&&)&& ref // ref is const std::string&&

Considérez la fonction de modèle la plus simple - min et max. En C ++ 03, vous devez surcharger manuellement les quatre combinaisons de const et non-const. En C ++ 0x, ce n'est qu'une surcharge. Combiné avec des modèles variadic, cela permet un transfert parfait.

template<typename A, typename B> auto min(A&& aref, B&& bref) {
    // for example, if you pass a const std::string& as first argument,
    // then A becomes const std::string& and by extension, aref becomes
    // const std::string&, completely maintaining it's type information.
    if (std::forward<A>(aref) < std::forward<B>(bref))
        return std::forward<A>(aref);
    else
        return std::forward<B>(bref);
}

J'ai laissé de côté la déduction du type de retour, car je ne me souviens pas comment cela se fait à la légère, mais ce min peut accepter n'importe quelle combinaison de lvalues, rvalues, const lvalues.

Chiot
la source
pourquoi avez-vous utilisé std::forward<A>(aref) < std::forward<B>(bref)? et je ne pense pas que cette définition sera correcte lorsque vous essayez d'avancer int&et float&. Mieux vaut supprimer un modèle de formulaire de type.
Yankes
25

Le terme T&& utilisé lorsqu'il est utilisé avec une déduction de type (comme pour un transfert parfait) est connu familièrement comme une référence de transfert . Le terme «référence universelle» a été inventé par Scott Meyers dans cet article , mais a ensuite été modifié.

En effet, il peut s'agir d'une valeur r ou d'une valeur l.

Voici des exemples:

// template
template<class T> foo(T&& t) { ... }

// auto
auto&& t = ...;

// typedef
typedef ... T;
T&& t = ...;

// decltype
decltype(...)&& t = ...;

Plus de discussion peut être trouvée dans la réponse pour: Syntaxe des références universelles

mmocny
la source
14

Une référence rvalue est un type qui se comporte un peu comme la référence ordinaire X &, à quelques exceptions près. Le plus important est que lorsqu'il s'agit de résolution de surcharge de fonction, les valeurs préfèrent les références lvalue à l'ancienne, tandis que rvalues ​​préfèrent les nouvelles références rvalue:

void foo(X& x);  // lvalue reference overload
void foo(X&& x); // rvalue reference overload

X x;
X foobar();

foo(x);        // argument is lvalue: calls foo(X&)
foo(foobar()); // argument is rvalue: calls foo(X&&)

Alors, quelle est une valeur? Tout ce qui n'est pas une valeur. Une valeur l étant une expression qui fait référence à un emplacement mémoire et nous permet de prendre l'adresse de cet emplacement mémoire via l'opérateur &.

Il est presque plus facile de comprendre d'abord ce que les valeurs accomplissent avec un exemple:

 #include <cstring>
 class Sample {
  int *ptr; // large block of memory
  int size;
 public:
  Sample(int sz=0) : ptr{sz != 0 ? new int[sz] : nullptr}, size{sz} 
  {
     if (ptr != nullptr) memset(ptr, 0, sz);
  }
  // copy constructor that takes lvalue 
  Sample(const Sample& s) : ptr{s.size != 0 ? new int[s.size] :\
      nullptr}, size{s.size}
  {
     if (ptr != nullptr) memcpy(ptr, s.ptr, s.size);
     std::cout << "copy constructor called on lvalue\n";
  }

  // move constructor that take rvalue
  Sample(Sample&& s) 
  {  // steal s's resources
     ptr = s.ptr;
     size = s.size;        
     s.ptr = nullptr; // destructive write
     s.size = 0;
     cout << "Move constructor called on rvalue." << std::endl;
  }    
  // normal copy assignment operator taking lvalue
  Sample& operator=(const Sample& s)
  {
   if(this != &s) {
      delete [] ptr; // free current pointer
      size = s.size;

      if (size != 0) {
        ptr = new int[s.size];
        memcpy(ptr, s.ptr, s.size);
      } else 
         ptr = nullptr;
     }
     cout << "Copy Assignment called on lvalue." << std::endl;
     return *this;
  }    
 // overloaded move assignment operator taking rvalue
 Sample& operator=(Sample&& lhs)
 {
   if(this != &s) {
      delete [] ptr; //don't let ptr be orphaned 
      ptr = lhs.ptr;   //but now "steal" lhs, don't clone it.
      size = lhs.size; 
      lhs.ptr = nullptr; // lhs's new "stolen" state
      lhs.size = 0;
   }
   cout << "Move Assignment called on rvalue" << std::endl;
   return *this;
 }
//...snip
};     

Le constructeur et les opérateurs d'affectation ont été surchargés de versions qui prennent des références rvalue. Les références Rvalue permettent à une fonction de se ramifier au moment de la compilation (via la résolution de surcharge) à la condition "Suis-je appelé sur une lvalue ou une rvalue?". Cela nous a permis de créer des constructeurs et des opérateurs d'affectation plus efficaces au-dessus de ceux qui déplacent les ressources plutôt que de les copier.

Le compilateur se branche automatiquement au moment de la compilation (selon qu'il est appelé pour une lvalue ou une rvalue) en choisissant si le constructeur de déplacement ou l'opérateur d'affectation de déplacement doit être appelé.

Pour résumer: les références rvalue permettent une sémantique de déplacement (et un transfert parfait, discuté dans le lien de l'article ci-dessous).

Un exemple pratique et facile à comprendre est le modèle de classe std :: unique_ptr . Puisqu'un unique_ptr conserve la propriété exclusive de son pointeur brut sous-jacent, unique_ptr ne peut pas être copié. Cela violerait leur invariant de propriété exclusive. Ils n'ont donc pas de constructeurs de copie. Mais ils ont des constructeurs de mouvements:

template<class T> class unique_ptr {
  //...snip
 unique_ptr(unique_ptr&& __u) noexcept; // move constructor
};

 std::unique_ptr<int[] pt1{new int[10]};  
 std::unique_ptr<int[]> ptr2{ptr1};// compile error: no copy ctor.  

 // So we must first cast ptr1 to an rvalue 
 std::unique_ptr<int[]> ptr2{std::move(ptr1)};  

std::unique_ptr<int[]> TakeOwnershipAndAlter(std::unique_ptr<int[]> param,\
 int size)      
{
  for (auto i = 0; i < size; ++i) {
     param[i] += 10;
  }
  return param; // implicitly calls unique_ptr(unique_ptr&&)
}

// Now use function     
unique_ptr<int[]> ptr{new int[10]};

// first cast ptr from lvalue to rvalue
unique_ptr<int[]> new_owner = TakeOwnershipAndAlter(\
           static_cast<unique_ptr<int[]>&&>(ptr), 10);

cout << "output:\n";

for(auto i = 0; i< 10; ++i) {
   cout << new_owner[i] << ", ";
}

output:
10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 

static_cast<unique_ptr<int[]>&&>(ptr)se fait généralement en utilisant std :: move

// first cast ptr from lvalue to rvalue
unique_ptr<int[]> new_owner = TakeOwnershipAndAlter(std::move(ptr),0);

Un excellent article expliquant tout cela et plus encore (comme la façon dont les valeurs permettent un transfert parfait et ce que cela signifie) avec de nombreux bons exemples est les références de valeur C ++ de Thomas Becker expliquées . Ce message s'appuyait fortement sur son article.

Une introduction plus courte est une brève introduction aux références de valeur par Stroutrup, et. Al

kurt krueckeberg
la source
N'est-ce pas pour que le constructeur de copie Sample(const Sample& s)doive aussi copier le contenu? La même question pour «l'opérateur d'affectation de copie».
K.Karamazen
Oui, tu as raison. Je n'ai pas réussi à copier la mémoire. Le constructeur de copie et l'opérateur d'affectation de copie doivent tous les deux faire memcpy (ptr, s.ptr, size) après avoir testé cette taille! = 0. Et le constructeur par défaut doit faire memset (ptr, 0, size) si size! = 0.
kurt krueckeberg
D'accord merci. Ainsi, ce commentaire et les deux commentaires précédents peuvent être supprimés car le problème a également été corrigé dans la réponse.
K.Karamazen