Che cosa significa T&& (doppia e commerciale) in C ++ 11?


801

Ho esaminato alcune delle nuove funzionalità di C ++ 11 e una che ho notato è la doppia e commerciale nel dichiarare variabili, come T&& var.

Per cominciare, come si chiama questa bestia? Vorrei che Google ci permettesse di cercare la punteggiatura in questo modo.

Che cosa significa esattamente ?

A prima vista, sembra essere un doppio riferimento (come i doppi puntatori in stile C T** var), ma faccio fatica a pensare a un caso d'uso per quello.


55
Ho aggiunto questo al c ++ - faq, poiché sono sicuro che uscirà di più in futuro.
GManNickG

3
domanda correlata sulla semantica degli spostamenti
fredoverflow

41
Puoi cercarlo utilizzando Google, devi solo racchiudere la tua frase tra virgolette: google.com/#q="T%26%26 "ora ha la tua domanda come primo successo. :)
sabato

C'è una risposta molto buona, facile da capire a una domanda simile qui stackoverflow.com/questions/7153991/…
Daniel

2
Ho ricevuto tre domande su StackOverflow in alto alla ricerca su Google per "parametro c ++ due e commerciali" e la tua è stata la prima. Quindi non è nemmeno necessario utilizzare la punteggiatura per questo se è possibile precisare "parametro con due e commerciali".
sergiol

Risposte:


668

Dichiara un riferimento al valore (doc proposta standard).

Ecco un'introduzione ai riferimenti di valore .

Ecco un fantastico approfondimento sui riferimenti di valore di uno degli sviluppatori di librerie standard di Microsoft .

ATTENZIONE: l'articolo collegato su MSDN ("Riferimenti Rvalue: funzionalità C ++ 0x in VC10, parte 2") è un'introduzione molto chiara ai riferimenti Rvalue, ma fa delle dichiarazioni sui riferimenti Rvalue che un tempo erano vere nella bozza C ++ 11 standard, ma non sono veri per quello finale! In particolare, in vari punti si dice che i riferimenti ai valori possono legarsi ai valori, che una volta era vero, ma è stato modificato (ad es. Int x; int && rrx = x; non compila più in GCC)

La più grande differenza tra un riferimento C ++ 03 (ora chiamato riferimento lvalue in C ++ 11) è che può legarsi a un valore come un temporaneo senza dover essere const. Pertanto, questa sintassi è ora legale:

T&& r = T();

i riferimenti rvalue prevedono principalmente quanto segue:

Sposta semantica . Ora è possibile definire un costruttore di movimenti e un operatore di assegnazione di movimenti che accetta un riferimento di valore anziché il normale riferimento di valore costante. Una mossa funziona come una copia, tranne per il fatto che non è obbligata a mantenere invariata la fonte; infatti, di solito modifica l'origine in modo tale da non possedere più le risorse spostate. Questo è ottimo per eliminare copie estranee, specialmente nelle implementazioni di librerie standard.

Ad esempio, un costruttore di copie potrebbe apparire così:

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

Se a questo costruttore fosse passato un temporaneo, la copia sarebbe superflua perché sappiamo che il temporaneo sarà semplicemente distrutto; perché non utilizzare le risorse temporanee già assegnate? In C ++ 03, non c'è modo di impedire la copia in quanto non possiamo determinare che ci è stato passato un temporaneo. In C ++ 11, possiamo sovraccaricare un costruttore di mosse:

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

Notate qui la grande differenza: il costruttore di mosse in realtà modifica il suo argomento. Ciò "sposterebbe" effettivamente il temporaneo nell'oggetto in costruzione, eliminando così la copia non necessaria.

Il costruttore di mosse verrebbe utilizzato per i provvisori e per i riferimenti a valori non cost che vengono esplicitamente convertiti in riferimenti a valore utilizzando la std::movefunzione (esegue semplicemente la conversione). Il codice seguente invoca il costruttore di mosse per f1e 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"

Inoltro perfetto . i riferimenti rvalue ci consentono di inoltrare correttamente argomenti per funzioni basate su modelli. Prendiamo ad esempio questa funzione di fabbrica:

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

Se lo chiamassimo factory<foo>(5), l'argomento verrà dedotto int&, il che non si legherà a un letterale 5, anche se fooil costruttore prende un int. Bene, potremmo invece usare A1 const&, ma cosa foosuccede se prende l'argomento del costruttore come riferimento non const? Per rendere una funzione di fabbrica veramente generica, dovremmo sovraccaricare la fabbrica A1&e accenderla A1 const&. Ciò potrebbe andare bene se la fabbrica accetta 1 tipo di parametro, ma ogni tipo di parametro aggiuntivo moltiplicherebbe il sovraccarico necessario impostato per 2. Questo è molto rapidamente non mantenibile.

i riferimenti rvalue risolvono questo problema consentendo alla libreria standard di definire una std::forwardfunzione in grado di inoltrare correttamente i riferimenti lvalue / rvalue. Per ulteriori informazioni su come std::forwardfunziona, vedere questa risposta eccellente .

Questo ci consente di definire la funzione di fabbrica in questo modo:

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

Ora il valore rvalue / lvalue-ness dell'argomento viene conservato quando viene passato al Tcostruttore. Ciò significa che se factory viene chiamato con un valore, Til costruttore viene chiamato con un valore. Se factory viene chiamato con un lvalue, Til costruttore viene chiamato con un lvalue. La funzione di fabbrica migliorata funziona a causa di una regola speciale:

Quando il tipo di parametro della funzione è nel formato in T&&cui si Ttrova un parametro del modello e l'argomento della funzione è un valore di tipo A, il tipo A&viene utilizzato per la deduzione dell'argomento del modello.

Quindi, possiamo usare factory in questo modo:

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

Importanti proprietà di riferimento del valore :

  • Per la risoluzione del sovraccarico, i lvalues ​​preferiscono l'associazione ai riferimenti ai valori e i rvalues ​​preferiscono l'associazione ai riferimenti rvalue . Ecco perché i provvisori preferiscono invocare un costruttore di spostamento / operatore di assegnazione di spostamento rispetto a un costruttore di copia / operatore di assegnazione.
  • i riferimenti al valore si legheranno implicitamente ai valori e ai provvisori che sono il risultato di una conversione implicita . cioè float f = 0f; int&& i = f;è ben formato perché float è implicitamente convertibile in int; il riferimento sarebbe a un temporaneo che è il risultato della conversione.
  • I riferimenti ai valori nominali sono valori. I riferimenti ai valori senza nome sono valori. Questo è importante per capire perché la std::movechiamata è necessaria in:foo&& r = foo(); foo f = std::move(r);

65
+1 per Named rvalue references are lvalues. Unnamed rvalue references are rvalues.; senza saperlo, ho faticato a capire perché le persone fanno T &&t; std::move(t);da molto tempo in mosse mediche e simili.
legends2k

@MaximYegorushkin: In quell'esempio, r è vincolante per un valore puro (temporaneo) e quindi il temporaneo dovrebbe estendere il suo ambito di vita, no?
Peter Huene,

@PeterHuene Riprendo ciò, un riferimento di valore r prolunga la durata di un temporaneo.
Maxim Egorushkin,

32
ATTENZIONE : l'articolo collegato su MSDN ("Riferimenti Rvalue: funzionalità C ++ 0x in VC10, parte 2") è un'introduzione molto chiara ai riferimenti Rvalue, ma fa delle dichiarazioni sui riferimenti Rvalue che un tempo erano vere nella bozza C ++ 11 standard, ma non sono veri per quello finale! In particolare, in vari punti si dice che i riferimenti ai valori possono legarsi ai valori, che una volta era vero, ma è stato modificato (ad esempio, int x; int &&rrx = x; non viene più compilato in GCC)
drewbarbs

@PeterHuene Nell'esempio sopra, non è typename identity<T>::type& aequivalente a T&?
ibp73,

81

Indica un riferimento al valore. I riferimenti di valore si legheranno solo a oggetti temporanei, a meno che non sia stato esplicitamente generato diversamente. Sono utilizzati per rendere gli oggetti molto più efficienti in determinate circostanze e per fornire una struttura nota come inoltro perfetto, che semplifica notevolmente il codice modello.

In C ++ 03, non è possibile distinguere tra una copia di un valore non modificabile e un valore.

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&);

In C ++ 0x, questo non è il caso.

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

Considera l'implementazione dietro questi costruttori. Nel primo caso, la stringa deve eseguire una copia per conservare la semantica del valore, che comporta una nuova allocazione dell'heap. Tuttavia, nel secondo caso, sappiamo in anticipo che l'oggetto che è stato passato al nostro costruttore è immediatamente dovuto alla distruzione e non deve rimanere intatto. Possiamo effettivamente scambiare i puntatori interni e non eseguire alcuna copia in questo scenario, che è sostanzialmente più efficiente. Le semantiche di spostamento avvantaggiano qualsiasi classe che disponga di copie costose o proibite di risorse con riferimenti interni. Considera il caso di std::unique_ptr- ora che la nostra classe è in grado di distinguere tra temporanei e non temporanei, possiamo far funzionare correttamente la semantica del movimento in modo tale che unique_ptrnon possa essere copiata ma possa essere spostata, il che significa chestd::unique_ptrpuò essere legalmente conservato in contenitori standard, ordinato, ecc., mentre C ++ 03 std::auto_ptrnon può.

Ora consideriamo l'altro uso dei riferimenti rvalue: l'inoltro perfetto. Considera la questione di associare un riferimento a un riferimento.

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

Non riesco a ricordare ciò che dice C ++ 03 a riguardo, ma in C ++ 0x, il tipo risultante quando si tratta di riferimenti a valori è fondamentale. Un riferimento di valore a un tipo T, dove T è un tipo di riferimento, diventa un riferimento di tipo 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&&

Considera la funzione modello più semplice - min e max. In C ++ 03 devi sovraccaricare manualmente tutte e quattro le combinazioni di const e non const. In C ++ 0x è solo un sovraccarico. Combinato con modelli variadici, questo consente un perfetto inoltro.

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);
}

Ho interrotto la deduzione del tipo restituito, perché non riesco a ricordare come è stato fatto a mano, ma quel minuto può accettare qualsiasi combinazione di valori, valori, valori costanti.


perché hai usato std::forward<A>(aref) < std::forward<B>(bref)? e non credo che questa definizione sarà corretta quando si tenta di avanzare int&e float&. Meglio rilasciare un modello di modulo di tipo.
Yankes

25

Il termine per T&& quando utilizzato con la deduzione del tipo (come per l'inoltro perfetto) è noto colloquialmente come riferimento di inoltro . Il termine "riferimento universale" è stato coniato da Scott Meyers in questo articolo , ma è stato successivamente modificato.

Questo perché può essere valore r o valore l.

Esempi sono:

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

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

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

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

Altre discussioni sono disponibili nella risposta per: Sintassi per riferimenti universali


14

Un riferimento al valore è un tipo che si comporta in modo molto simile al normale riferimento X e, con diverse eccezioni. Il più importante è che quando si tratta di funzionare con la risoluzione di sovraccarico, i valori di lval preferiscono riferimenti a valori di vecchio stile, mentre i valori preferiscono i nuovi riferimenti di valore:

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&&)

Quindi cos'è un valore? Tutto ciò che non è un valore. Un lvalue è un'espressione che si riferisce a una posizione di memoria e ci consente di prendere l'indirizzo di quella posizione di memoria tramite l'operatore &.

È quasi più facile capire prima cosa realizzano i valori con un esempio:

 #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
};     

Gli operatori di costruzione e assegnazione sono stati sovraccaricati di versioni che accettano riferimenti a valori. I riferimenti di valore consentono a una funzione di ramificarsi in fase di compilazione (tramite risoluzione di sovraccarico) a condizione "Sono stato chiamato su un valore o un valore?". Questo ci ha permesso di creare più efficienti operatori di costruzione e assegnazione sopra che spostano le risorse piuttosto che copiarle.

Il compilatore si dirama automaticamente al momento della compilazione (a seconda che venga invocato per un valore o un valore) scegliendo se chiamare il costruttore dello spostamento o l'operatore di assegnazione dello spostamento.

Riassumendo: i riferimenti di valore consentono la semantica di movimento (e l'inoltro perfetto, discusso nel link dell'articolo qui sotto).

Un esempio pratico e di facile comprensione è il modello di classe std :: unique_ptr . Poiché unique_ptr mantiene la proprietà esclusiva del puntatore non elaborato sottostante, non è possibile copiare quelli di unique_ptr. Ciò violerebbe il loro invariante di proprietà esclusiva. Quindi non hanno costruttori di copie. Ma hanno costruttori di mosse:

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)di solito viene fatto usando std :: move

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

Un eccellente articolo che spiega tutto questo e altro (come il modo in cui i valori consentono l'inoltro perfetto e cosa significa) con molti buoni esempi è spiegato dai riferimenti al valore C ++ di Thomas Becker . Questo post faceva molto affidamento sul suo articolo.

Un'introduzione più breve è una breve introduzione ai riferimenti di valore di Stroutrup, et. al


Non è così che anche il costruttore di copie Sample(const Sample& s)deve copiare i contenuti? La stessa domanda per "l'operatore di assegnazione della copia".
K.Karamazen,

Si hai ragione. Non sono riuscito a copiare la memoria. Il costruttore della copia e l'operatore di assegnazione della copia dovrebbero entrambi fare memcpy (ptr, s.ptr, dimensione) dopo aver testato quella dimensione! = 0. E il costruttore predefinito dovrebbe fare memset (ptr, 0, dimensione) se size! = 0.
kurt krueckeberg,

Va bene, grazie. Pertanto, questo commento e i due commenti precedenti possono essere rimossi perché il problema è stato anche corretto nella risposta.
K.Karamazen,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.