Wat doet T&& (dubbele ampersand) gemiddelde in C++11?

Ik heb gekeken naar enkele van de nieuwe functies van C++11 en een die me is opgevallen is de dubbele ampersand bij het declareren van variabelen, zoals T&& var.

Om te beginnen, hoe heet dit beest? Ik wou dat Google ons zou toestaan om op deze manier naar interpunctie te zoeken.

Wat betekent het precies ?

Op het eerste gezicht lijkt het een dubbele referentie (zoals de C-stijl dubbele pointers T** var), maar ik vind het moeilijk om daar een gebruiksscenario voor te bedenken .


Antwoord 1, autoriteit 100%

Het verklaart een rvalue-referentie(document voor normvoorstel).

Hier is een inleiding tot rvalue referenties.

Hier is een fantastische diepgaande blik op rvalue-referenties door een van de standaardbibliotheken van Microsoft ontwikkelaars.

LET OP:het gelinkte artikel op MSDN (“Rvalue References: C++0x Features in VC10, Part 2”) is een zeer duidelijke introductie tot Rvalue-referenties, maar doet uitspraken over Rwaarde-referenties die ooit waar waren in de concept-C++11-standaard, maar niet waar zijn voor de laatste! In het bijzonder staat er op verschillende punten dat rvalue-referenties kunnen binden aan lvalues, wat ooit waar was, maar werd gewijzigd (bijv. int x; int &&rrx = x; compileert niet langer in GCC) – Drawbarbs Jul 13 ’14 om 16:12

Het grootste verschil tussen een C++03-referentie (nu een lvalu-referentie genoemd in C++11) is dat het kan binden aan een rvalue als een tijdelijke zonder dat het const hoeft te zijn. Deze syntaxis is nu dus legaal:

T&& r = T();

rvalue-referenties bieden voornamelijk het volgende:

Semantiek verplaatsen. Een move-constructor en move-toewijzingsoperator kunnen nu worden gedefinieerd die een rvalue-referentie nodig hebben in plaats van de gebruikelijke const-lvalue-referentie. Een zet werkt als een kopie, behalve dat het niet verplicht is de bron ongewijzigd te laten; in feite wijzigt het de bron meestal zodanig dat het niet langer eigenaar is van de verplaatste bronnen. Dit is geweldig voor het elimineren van overbodige kopieën, vooral in standaard bibliotheekimplementaties.

Een kopie-constructor kan er bijvoorbeeld als volgt uitzien:

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

Als deze constructor een tijdelijk gegeven zou hebben, zou de kopie niet nodig zijn omdat we weten dat de tijdelijke gewoon vernietigd zal worden; waarom geen gebruik maken van de reeds toegewezen middelen? In C++03 is er geen manier om de kopie te voorkomen, omdat we niet kunnen bepalen of we een tijdelijke. In C++11 kunnen we een move-constructor overbelasten:

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

Let hier op het grote verschil: de move-constructor wijzigt in feite zijn argument. Dit zou het tijdelijke in feite “verplaatsen” in het object dat wordt geconstrueerd, waardoor de onnodige kopie wordt geëlimineerd.

De move-constructor zou worden gebruikt voor tijdelijke en voor niet-const-lvalue-referenties die expliciet worden geconverteerd naar rvalue-referenties met behulp van de functie std::move(het voert alleen de conversie uit). De volgende code roept beide de move-constructor op voor f1en 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"

Perfecte doorzending. rvalu-referenties stellen ons in staat om argumenten voor sjabloonfuncties op de juiste manier door te sturen. Neem bijvoorbeeld deze fabrieksfunctie:

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

Als we factory<foo>(5)hebben aangeroepen, wordt het argument afgeleid als int&, wat niet bindt aan een letterlijke 5, zelfs als foo‘s constructor neemt een int. Welnu, we zouden in plaats daarvan A1 const&kunnen gebruiken, maar wat als foohet constructorargument door niet-constreferentie gebruikt? Om een echt generieke fabrieksfunctie te maken, zouden we de fabriek op A1&en op A1 const&moeten overbelasten. Dat kan prima zijn als de fabriek 1 parametertype gebruikt, maar elk extra parametertype zou de benodigde overbelasting met 2 vermenigvuldigen. Dat is al snel onhoudbaar.

rvalue-referenties lossen dit probleem op door de standaardbibliotheek een functie std::forwardte laten definiëren die lvalue/rvalu-referenties correct kan doorsturen. Zie dit uitstekende antwoordvoor meer informatie over hoe std::forwardwerkt.

Hierdoor kunnen we de fabrieksfunctie als volgt definiëren:

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

Nu blijft de rvalue/lvalue-ness van het argument behouden wanneer deze wordt doorgegeven aan de constructor van T. Dat betekent dat als fabriek wordt aangeroepen met een rwaarde, de constructor van Twordt aangeroepen met een rwaarde. Als factory wordt aangeroepen met een lvalue, wordt de constructor van Taangeroepen met een lvalue. De verbeterde fabrieksfunctie werkt vanwege één speciale regel:

Als het functieparametertype van . is
de vorm T&&waarbij Teen sjabloon is
parameter en het functieargument
is een lwaarde van het type A, het type A&is
gebruikt voor deductie van sjabloonargumenten.

We kunnen de fabriek dus als volgt gebruiken:

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

Belangrijke rvalue-referentie-eigenschappen:

  • Voor het oplossen van overbelasting geven lvalues de voorkeur aan binding met lvalu-referenties en rvalues aan binding aan rvalu-referenties. Vandaar dat temporaries de voorkeur geven aan het aanroepen van een move constructor / move toewijzingsoperator boven een kopieerconstructor / toewijzingsoperator.
  • rvalue-referenties zullen impliciet binden aan rvalues en aan tijdelijke waarden die het resultaat zijn van een impliciete conversie. d.w.z. float f = 0f; int&& i = f;is goed gevormd omdat float impliciet converteerbaar is naar int; de verwijzing zou zijn naar een tijdelijke die het resultaat is van de conversie.
  • Benoemde rvalue-referenties zijn lvalues. Naamloze rvalue-referenties zijn rvalues.Dit is belangrijk om te begrijpen waarom de aanroep std::movenodig is in: foo&& r = foo(); foo f = std::move(r);

Antwoord 2, autoriteit 12%

Het geeft een rvalue-referentie aan. Rvalu-referenties binden alleen aan tijdelijke objecten, tenzij expliciet anders gegenereerd. Ze worden gebruikt om objecten onder bepaalde omstandigheden veel efficiënter te maken en om een faciliteit te bieden die bekend staat als perfect forwarding, wat de sjablooncode aanzienlijk vereenvoudigt.

In C++03 kun je geen onderscheid maken tussen een kopie van een niet-veranderbare l-waarde en een r-waarde.

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 is dit niet het geval.

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

Overweeg de implementatie achter deze constructors. In het eerste geval moet de string een kopie uitvoeren om de waardesemantiek te behouden, wat een nieuwe heaptoewijzing inhoudt. In het tweede geval weten we echter van tevoren dat het object dat is ingeleverd bij onze constructeur onmiddellijk aan vernietiging toe is en niet onaangeroerd hoeft te blijven. In dit scenario kunnen we in feite gewoon de interne aanwijzers verwisselen en helemaal niet kopiëren, wat aanzienlijk efficiënter is. Verplaatssemantiek komt ten goede aan elke klasse die dure of verboden kopiëren van intern gerefereerde bronnen heeft. Beschouw het geval van std::unique_ptr– nu onze klasse onderscheid kan maken tussen tijdelijke en niet-tijdelijke, kunnen we de verplaatsingssemantiek correct laten werken, zodat de unique_ptrniet kan worden gekopieerd maar kan worden verplaatst, wat betekent dat std::unique_ptrlegaal kan worden opgeslagen in standaardcontainers, gesorteerd, enz., terwijl std::auto_ptrvan C++03 dat niet kan.

Nu bekijken we het andere gebruik van rvalue-referenties: perfect doorsturen. Overweeg de kwestie van het binden van een verwijzing aan een verwijzing.

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

Ik kan me niet herinneren wat C++03 hierover zegt, maar in C++0x is het resulterende type bij het omgaan met rvalu-referenties van cruciaal belang. Een rwaarde-referentie naar een type T, waarbij T een referentietype is, wordt een referentie van het 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&&

Overweeg de eenvoudigste sjabloonfunctie – min en max. In C++03 moet je alle vier de combinaties van const en non-const handmatig overbelasten. In C++0x is het slechts één overbelasting. In combinatie met variadische sjablonen maakt dit een perfecte doorzending mogelijk.

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

Ik heb de aftrek van het retourtype achterwege gelaten, omdat ik me niet kan herinneren hoe het terloops is gedaan, maar die min kan elke combinatie van lvalues, rvalues, const lvalues accepteren.


Antwoord 3, autoriteit 4%

De term voor T&&wanneer gebruikt met typeaftrek(zoals voor perfect doorsturen) staat in de volksmond bekend als een doorstuurreferentie. De term “universele referentie” is bedacht door Scott Meyers in deze artikel, maar werd later gewijzigd.

Dat komt omdat het r-waarde of l-waarde kan zijn.

Voorbeelden zijn:

// template
template<class T> foo(T&& t) { ... }
// auto
auto&& t = ...;
// typedef
typedef ... T;
T&& t = ...;
// decltype
decltype(...)&& t = ...;

Meer discussie is te vinden in het antwoord voor: Syntaxis voor universele referenties


Antwoord 4, Autoriteit 3%

Een RVALUE-referentie is een type dat zich veel op de gewone referentie X & AMP ;, met verschillende uitzonderingen. De belangrijkste is dat als het gaat om de resolutie van de functie overbelasting, LVALUES de voorkeur aan oude-stijl LVALUE-referenties, terwijl RVALUES de voorkeur geven aan de nieuwe RVALUE-referenties:

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

Dus wat is een rvalue? Alles dat geen LVALUE is. Een lvalue wezen
Een uitdrukking die verwijst naar een geheugenlocatie en stelt ons in staat om het adres van die geheugenlocatie via de & AMP te nemen; operator.

Het is bijna gemakkelijker te begrijpen welke rvalues ​​het bereiken met een voorbeeld:

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

De operators van de constructeur en toewijzing zijn overbelast met versies die RVALUE-referenties nemen. RVALUE REFERENTIES Doe een functie toe aan tak op compileertijd (via overbelastingresolutie) op de voorwaarde “ben ik opgeroepen op een LVALUE of een Rvalue?”. Hierdoor konden we een efficiëntere constructor- en toewijzingsoperators maken die bovendien middelen worden verplaatst die ze eerder kopiëren.

De compiler vertakt automatisch op compileertijd (afhankelijk van de of het wordt aangeroepen voor een LVALUE of een RVALUE), het kiezen of de Verplaats Constructor of Move Toewijzing Operator moet worden genoemd.

SAMENVATTING: RVALUE REFERENTIES Zorg voor verplaatsen Semantiek (en perfecte doorsturen, besproken in de onderstaande artikel).

Een praktisch, gemakkelijk te begrijpen voorbeeld is het klassensjabloon std::unique_ptr. Aangezien een unique_ptr het exclusieve eigendom behoudt van zijn onderliggende onbewerkte pointer, kunnen unique_ptr’s niet worden gekopieerd. Dat zou hun invariant van exclusief eigendom schenden. Ze hebben dus geen copy-constructors. Maar ze hebben wel move-constructors:

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)wordt meestal gedaan met std::move

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

Een uitstekend artikel dat alles en meer uitlegt (zoals hoe RVALUES Perfecte doorsturen en wat dat betekent) met veel goede voorbeelden is Thomas Becker’s C++ RVALUE referenties uitgelegd . Dit bericht vertrouwde zwaar op zijn artikel.

Een kortere introductie is een korte introductie tot Rvalue referenties door StrrouTrup, ET. al

Other episodes