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


J'ai examiné quelques-unes des nouvelles fonctionnalités de C ++ 11 et un que j'ai remarqué est la double esperluette dans la déclaration des variables, comme T&& var.

Pour commencer, comment s'appelle cette bête? Je souhaite que Google nous permette de rechercher la ponctuation comme ceci.

Qu'est-ce que c'est exactement? signifier?

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


608
2018-03-30 03:29


origine


Réponses:


Il déclare un référence rvalue (document de proposition de normes).

Voici une introduction à rvalue les références.

Voici un aperçu approfondi des références rvalue par l'une des bibliothèques standard de Microsoft les développeurs. (Mais voir la mise en garde dans les commentaires qui suivent cette réponse avant de lire cet article.)

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

T&& r = T();

Les références de rvalue prévoient principalement ce qui suit:

Déplacer la sémantique. Un constructeur de mouvement et un opérateur d'assignation de mouvement peuvent maintenant être définis qui prennent une référence rvalue au lieu de la référence habituelle de const-lvalue. 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 telle sorte qu'elle ne possède plus les ressources déplacées. C'est excellent pour éliminer les copies superflues, en particulier dans les implémentations de bibliothèques 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 était passé temporaire, la copie serait inutile car nous savons que le temporaire sera simplement détruit; pourquoi ne pas utiliser les ressources que le temporaire a 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 été passés un temporaire. En C ++ 11, nous pouvons surcharger un constructeur de déplacement:

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 réellement son argument. Cela permettrait de "déplacer" le temporaire dans l'objet en cours de construction, éliminant ainsi la copie inutile.

Le constructeur de mouvement serait utilisé pour les références temporaires et pour les références de lvalue non-const qui sont explicitement converties en références rvalue en utilisant le std::move fonction (il effectue juste la conversion). Le code suivant appelle le constructeur de déplacement pour f1 et 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 pour les fonctions basées sur un 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 avons appelé factory<foo>(5), l'argument sera déduit pour être int&, qui ne liera pas à un littéral 5, même si foole constructeur prend un int. Eh bien, nous pourrions plutôt utiliser A1 const&, mais si foo prend l'argument constructeur par référence non-const? Pour faire une fonction d'usine vraiment générique, nous devrions surcharger l'usine A1&et sur A1 const&. Cela peut être correct si l'usine prend un type de paramètre, mais chaque type de paramètre supplémentaire multiplierait la surcharge nécessaire définie par 2. Cela est très rapidement impossible à maintenir.

Les références rvalue corrigent ce problème en permettant à la bibliothèque standard de définir un std::forward fonction qui peut renvoyer correctement les références lvalue / rvalue. Pour plus d'informations sur comment std::forward fonctionne, voir 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)));
}

Maintenant, l'argument rvalue / lvalue-ness de l'argument est conservé lorsqu'il est passé à Tle constructeur. Cela signifie que si l'usine est appelée avec une valeur, TLe constructeur est appelé avec un rvalue. Si l'usine est appelée avec une lvalue, TLe constructeur est appelé avec une lvalue. La fonction d'usine améliorée fonctionne à cause d'une règle spéciale:

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

Ainsi, nous pouvons utiliser l'usine comme ça:

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, les lvalues ​​préfèrent se lier aux références lvalue et les valeurs préfèrent se lier aux références rvalue. C'est pourquoi les temporaires préfèrent appeler un opérateur de déplacement / assignement de mouvement sur un constructeur de copie / un 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. c'est à dire. float f = 0f; int&& i = f; est bien formé parce que float est implicitement convertible en int; la référence serait à un temporaire qui est le résultat de la conversion.
  • Les références rvalue nommées sont des valeurs lues. Les références de rvalue sans nom sont des valeurs.  C'est important de comprendre pourquoi std::move appel est nécessaire dans: foo&& r = foo(); foo f = std::move(r);

542
2018-03-30 04:04



Il dénote une référence rvalue. Les références Rvalue se lient uniquement aux objets temporaires, sauf si elles sont explicitement générées autrement. Ils sont utilisés pour rendre les objets beaucoup plus efficaces dans certaines circonstances, et pour fournir une fonction connue sous le nom de transfert parfait, ce qui simplifie grandement le code du modèle.

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

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 second cas, nous savons d'avance que l'objet qui a été transmis à notre constructeur doit être détruit immédiatement et qu'il ne doit pas rester intact. Nous pouvons simplement échanger les pointeurs internes et ne pas effectuer de copie dans ce scénario, ce qui est nettement plus efficace. La sémantique de déplacement bénéficie à toute classe qui a une copie coûteuse ou interdite de ressources référencées en interne. Considérons le cas de std::unique_ptr- Maintenant que notre classe peut distinguer entre les temporels et les non-temporaires, nous pouvons faire fonctionner correctement la sémantique unique_ptrne peut pas être copié mais peut être déplacé, ce qui signifie que std::unique_ptr peut être légalement stocké dans des conteneurs standard, triés, etc, alors que C ++ 03 std::auto_ptr ne peux pas.

Maintenant, nous considérons l'autre utilisation des références rvalue - transmission parfaite. Considérons 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

Impossible de se rappeler ce que C ++ 03 dit à ce sujet, mais en C ++ 0x, le type résultant en traitant 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érons la fonction de modèle la plus simple: min et max. En C ++ 03, vous devez surcharger manuellement les quatre combinaisons const et non-const. En C ++ 0x c'est juste une surcharge. Combiné avec des modèles variés, 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, parce que je ne peux pas me rappeler comment cela se fait en main propre, mais que min peut accepter n'importe quelle combinaison de lvalues, de valeurs, de valeurs const.


76
2018-01-17 14:21



Le terme pour T&&  lorsqu'il est utilisé avec la déduction de type (comme pour l'expédition parfaite) est connu familièrement comme référence universelle. Cela a été inventé par Scott Meyers dans cet article.

C'est parce que ce peut être soit une valeur r ou une valeur l.

Les exemples sont:

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

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

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

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

Notez que la norme elle-même n'a aucune notion de ceci, c'est simplement un moyen de discuter de la combinaison (impaire?) Des règles de réduction de référence, de la déduction du type de référence et de la syntaxe &&.

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


18
2017-11-03 15:31



Une référence rvalue est un type qui se comporte beaucoup comme la référence ordinaire X &, à quelques exceptions près. Le plus important est que lorsqu'il s'agit de la résolution de surcharge de fonction, les lvalues ​​préfèrent les références lvalue de style ancien, alors que les valeurs de rvalue préfèrent les nouvelles références de 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, qu'est-ce qu'une valeur? Tout ce qui n'est pas une lvalue. Un lvalue étant une expression qui fait référence à un emplacement de mémoire et nous permet de prendre l'adresse de cet emplacement de mémoire via l'opérateur &.

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

 class Sample {
  int *ptr; // large block of memory
  int size;
 public:
  Sample(int sz=0) : ptr{sz != 0 ? new int[sz] : nullptr}, size{sz} 
  {}
  // copy constructor that takes lvalue 
  Sample(const Sample& s) : ptr{s.size != 0 ? new int[s.size] :\
      nullptr}, size{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
      ptr = new int[s.size]; 
      size = s.size; 
    }
    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
};     

Les opérateurs constructeur et affectation ont été surchargés avec des 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 une résolution de surcharge) à la condition "Suis-je appelé sur une lvalue ou une rvalue?". Cela nous a permis de créer des opérateurs de constructeur et d'affectation plus efficaces que les ressources de déplacement plutôt que de les copier. 

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

En résumé: les références rvalue permettent une sémantique de déplacement (et un renvoi 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. Comme unique_ptr maintient la propriété exclusive de son pointeur brut sous-jacent, les uniques_ptr ne peuvent pas être copiés. Cela violerait leur invariant de propriété exclusive. Ils n'ont donc pas de constructeur de copie. Mais ils ont des constructeurs de mouvement:

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)est habituellement fait en utilisant std :: bouger

// 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 (comme la façon dont les valeurs permettent une transmission parfaite et ce que cela signifie) avec beaucoup de bons exemples est celui de Thomas Becker Références C ++ Rvalue expliquées. Ce post s'appuyait fortement sur son article.

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


7