1087 Stimmen

Was bedeutet T&& (double ampersand) in C++11?

Ich habe mir einige der neuen Funktionen von C++11 angeschaut, und eine davon ist das doppelte kaufmännische Und bei der Deklaration von Variablen, wie T&& var .

Zunächst einmal: Wie heißt dieses Ungeheuer? Ich wünschte, Google würde uns erlauben, nach Satzzeichen wie diesem zu suchen.

Was genau bedeutet es bedeuten?

Auf den ersten Blick scheint es sich um eine Doppelreferenz zu handeln (wie die Doppelzeiger im C-Stil T** var ), aber es fällt mir schwer, mir einen Anwendungsfall dafür vorzustellen.

857voto

Peter Huene Punkte 5678

Sie erklärt eine rWert-Referenz (Normenvorschlag doc).

Hier ist eine Einführung in rvalue Referenzen .

Hier ist eine fantastische eingehenden Blick auf rvalue Referenzen von einem von Microsofts Standard-Bibliothek Entwickler .

VORSICHT! der verlinkte Artikel auf MSDN ("Rvalue References: C++0x Features in VC10, Part 2") ist eine sehr klare Einführung in Rvalue-Referenzen, macht aber Aussagen über Rvalue-Referenzen, die einst im Entwurf des C++11-Standards wahr waren, aber für den endgültigen Standard nicht mehr gelten! Insbesondere heißt es an verschiedenen Stellen, dass rvalue-Referenzen an lvalues binden können, was einmal wahr war, aber geändert wurde.(z.B. int x; int &&rrx = x; kompiliert nicht mehr in GCC) - drewbarbs Jul 13 '14 at 16:12

Der größte Unterschied zwischen einer C++03-Referenz (in C++11 jetzt lvalue-Referenz genannt) besteht darin, dass sie wie ein temporärer Wert an einen rvalue binden kann, ohne const sein zu müssen. Somit ist diese Syntax jetzt legal:

T&& r = T();

rvalue-Referenzen bieten in erster Linie folgende Möglichkeiten:

Semantik verschieben . Ein move-Konstruktor und ein move-Zuweisungsoperator können nun definiert werden, die eine rvalue-Referenz anstelle der üblichen const-lvalue-Referenz annehmen. Eine Verschiebung funktioniert wie eine Kopie, mit dem Unterschied, dass die Quelle nicht unverändert bleibt, sondern normalerweise so verändert wird, dass sie die verschobenen Ressourcen nicht mehr besitzt. Dies eignet sich hervorragend, um überflüssige Kopien zu eliminieren, insbesondere in Standardbibliotheksimplementierungen.

Ein Kopierkonstruktor könnte zum Beispiel wie folgt aussehen:

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

Würde diesem Konstruktor ein temporäres Element übergeben, wäre die Kopie unnötig, da wir wissen, dass das temporäre Element einfach zerstört wird; warum also nicht die Ressourcen nutzen, die das temporäre Element bereits zugewiesen hat? In C++03 gibt es keine Möglichkeit, die Kopie zu verhindern, da wir nicht feststellen können, ob uns ein temporäres Element übergeben wurde. In C++11 können wir einen move-Konstruktor überladen:

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

Der große Unterschied ist, dass der Konstruktor move sein Argument tatsächlich ändert. Dadurch wird das temporäre Objekt effektiv in das zu konstruierende Objekt "verschoben", wodurch die unnötige Kopie entfällt.

Der move-Konstruktor wird für temporäre Werte und für nicht-konstante l-Wert-Referenzen verwendet, die explizit in r-Wert-Referenzen umgewandelt werden, indem die std::move Funktion (sie führt nur die Konvertierung durch). Der folgende Code ruft beide den Konstruktor move für f1 y 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"

Perfekte Weiterleitung . rvalue-Referenzen ermöglichen es uns, Argumente für Vorlagenfunktionen korrekt weiterzuleiten. Nehmen Sie zum Beispiel diese Fabrikfunktion:

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

Wenn wir die factory<foo>(5) wird das Argument wie folgt hergeleitet int& die nicht an eine 5 gebunden wird, auch wenn foo Konstruktor nimmt eine int . Nun, wir könnten stattdessen Folgendes verwenden A1 const& aber was wäre, wenn foo das Konstruktorargument als Nicht-Konst-Referenz übernimmt? Um eine wirklich generische Fabrikfunktion zu erstellen, müssten wir factory überladen auf A1& und weiter A1 const& . Das mag in Ordnung sein, wenn die Fabrik nur einen Parametertyp annimmt, aber jeder weitere Parametertyp würde die erforderliche Überlastmenge mit 2 multiplizieren. Das ist sehr schnell unwartbar.

rvalue-Referenzen beheben dieses Problem, indem sie es der Standardbibliothek ermöglichen, eine std::forward Funktion, die lvalue/rvalue-Referenzen korrekt weiterleiten kann. Für weitere Informationen darüber, wie std::forward Werke, siehe diese ausgezeichnete Antwort .

Dies ermöglicht es uns, die Fabrikfunktion wie folgt zu definieren:

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

Jetzt bleibt die rvalue/lvalue-ness des Arguments erhalten, wenn es an T Konstrukteur. Das bedeutet, dass wenn factory mit einem rWert aufgerufen wird, T Konstruktor mit einem r-Wert aufgerufen wird. Wenn factory mit einem l-Wert aufgerufen wird, T Konstruktor mit einem l-Wert aufgerufen wird. Die verbesserte Fabrikfunktion funktioniert aufgrund einer besonderen Regel:

Wenn der Funktionsparameter vom Typ der Form T&&T ist eine Vorlage Parameter, und das Funktionsargument ist ein l-Wert vom Typ A der Typ A& ist für die Ableitung von Vorlagenargumenten verwendet.

Wir können die Fabrik also wie folgt verwenden:

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

Wichtige rvalue-Referenzeigenschaften :

  • Für die Überlastauflösung, lWerte bevorzugen die Bindung an lWert-Referenzen und rWerte die Bindung an rWert-Referenzen . Daher bevorzugen Provisorien den Aufruf eines Verschiebekonstruktors / Verschiebezuweisungsoperators gegenüber einem Kopierkonstruktor / Zuweisungsoperator.
  • rvalue-Referenzen werden implizit an rvalues und an temporäre Werte gebunden, die das Ergebnis einer impliziten Konvertierung sind . d.h. float f = 0f; int&& i = f; ist wohlgeformt, da float implizit in int konvertierbar ist; der Verweis würde auf ein temporäres Element erfolgen, das das Ergebnis der Konvertierung ist.
  • Benannte rWert-Referenzen sind lWerte. Unbenannte rWert-Referenzen sind rWerte. Dies ist wichtig, um zu verstehen, warum die std::move ist ein Anruf erforderlich: foo&& r = foo(); foo f = std::move(r);

103voto

Puppy Punkte 141483

Er bezeichnet eine rvalue-Referenz. Rvalue-Referenzen binden sich nur an temporäre Objekte, es sei denn, sie werden ausdrücklich anders erzeugt. Sie werden verwendet, um Objekte unter bestimmten Umständen viel effizienter zu machen und um eine Möglichkeit zu bieten, die als perfekte Weiterleitung bekannt ist und den Vorlagencode stark vereinfacht.

In C++03 kann man nicht zwischen einer Kopie eines nicht veränderbaren lWerts und eines rWerts unterscheiden.

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 ist dies nicht der Fall.

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

Betrachten Sie die Implementierung hinter diesen Konstruktoren. Im ersten Fall muss die Zeichenkette eine "copy to retain value"-Semantik durchführen, was eine neue Heap-Zuweisung erfordert. Im zweiten Fall wissen wir jedoch im Voraus, dass das Objekt, das unserem Konstruktor übergeben wurde, sofort zerstört werden muss, und es muss nicht unberührt bleiben. In diesem Szenario können wir einfach die internen Zeiger austauschen und müssen überhaupt keine Kopiervorgänge durchführen, was wesentlich effizienter ist. Die Move-Semantik kommt jeder Klasse zugute, in der das Kopieren von intern referenzierten Ressourcen teuer oder verboten ist. Betrachten wir den Fall von std::unique_ptr - Da unsere Klasse nun zwischen temporären und nicht-temporären Elementen unterscheiden kann, können wir die Verschiebungssemantik korrekt anwenden, so dass die unique_ptr können nicht kopiert, aber verschoben werden, was bedeutet, dass std::unique_ptr legal in Standardcontainern gespeichert, sortiert usw. werden können, während die C++03 std::auto_ptr nicht.

Betrachten wir nun die andere Verwendung von rvalue-Referenzen - die perfekte Weiterleitung. Betrachten wir die Frage der Bindung einer Referenz an eine Referenz.

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

Ich kann mich nicht erinnern, was C++03 dazu sagt, aber in C++0x ist der resultierende Typ beim Umgang mit rvalue-Referenzen entscheidend. Eine rvalue-Referenz auf einen Typ T, wobei T ein Referenztyp ist, wird zu einer Referenz vom Typ 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&&

Betrachten wir die einfachste Schablonenfunktion - min und max. In C++03 müssen Sie für alle vier Kombinationen von const und non-const manuell überladen. In C++0x ist es nur eine Überladung. In Kombination mit variadischen Vorlagen ermöglicht dies eine perfekte Weiterleitung.

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

Ich habe die Ableitung des Rückgabetyps weggelassen, weil ich mich nicht aus dem Stegreif daran erinnern kann, wie das gemacht wird, aber dieser min kann jede Kombination von lvalues, rvalues, const lvalues akzeptieren.

29voto

mmocny Punkte 8575

Der Begriff für T&& bei Verwendung des Typenabzugs (z. B. für perfekte Weiterleitung) wird umgangssprachlich als Weiterleitungshinweis . Der Begriff "universelle Referenz" wurde von Scott Meyers geprägt in diesem Artikel , wurde aber später geändert.

Das liegt daran, dass es sich entweder um einen r-Wert oder einen l-Wert handeln kann.

Beispiele sind:

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

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

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

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

Weitere Informationen finden Sie in der Antwort für: Syntax für universelle Referenzen

23voto

kurt krueckeberg Punkte 321

Eine rvalue-Referenz ist ein Typ, der sich ähnlich verhält wie die gewöhnliche Referenz X&, mit einigen Ausnahmen. Die wichtigste davon ist, dass lvalues bei der Auflösung von Funktionsüberladungen lvalue-Referenzen im alten Stil bevorzugen, während rvalues die neuen rvalue-Referenzen bevorzugen:

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

Was ist also ein rvalue? Alles, was nicht ein l-Wert ist. Ein l-Wert ist ein Ausdruck, der sich auf eine Speicherstelle bezieht und es uns ermöglicht, die Adresse dieser Speicherstelle mit dem Operator & zu übernehmen.

Es ist fast einfacher, zunächst anhand eines Beispiels zu verstehen, was rvalues leisten:

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

Die Konstruktor- und Zuweisungsoperatoren wurden mit Versionen überladen, die rvalue-Referenzen annehmen. R-Wert-Referenzen ermöglichen es einer Funktion, zur Kompilierzeit (über die Überladungsauflösung) auf die Bedingung "Werde ich mit einem l-Wert oder einem r-Wert aufgerufen?" zu verzweigen. Dies ermöglichte es uns, effizientere Konstruktor- und Zuweisungsoperatoren zu erstellen, die Ressourcen verschieben anstatt sie zu kopieren.

Der Compiler verzweigt automatisch zur Kompilierzeit (je nachdem, ob er für einen l-Wert oder einen r-Wert aufgerufen wird) und wählt aus, ob der move-Konstruktor oder der move-Zuweisungsoperator aufgerufen werden soll.

Zusammenfassend lässt sich sagen, dass rvalue-Referenzen eine Verschiebungssemantik ermöglichen (und eine perfekte Weiterleitung, die in dem unten verlinkten Artikel besprochen wird).

Ein praktisches, leicht zu verstehendes Beispiel ist die Klassenvorlage std::unique_ptr . Da ein unique_ptr das ausschließliche Eigentum an seinem zugrundeliegenden Rohzeiger beibehält, können unique_ptr nicht kopiert werden. Das würde ihre Invariante des exklusiven Eigentums verletzen. Daher haben sie keine Kopierkonstruktoren. Aber sie haben Move-Konstruktoren:

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) erfolgt in der Regel über std::move

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

Ein hervorragender Artikel, der all dies und mehr erklärt (z.B. wie rvalues eine perfekte Weiterleitung ermöglichen und was das bedeutet) mit vielen guten Beispielen ist Thomas Beckers C++ Rvalue-Referenzen erklärt . Dieser Beitrag stützt sich stark auf seinen Artikel.

Eine kürzere Einführung ist Eine kurze Einführung in Rvalue-Referenzen von Stroutrup, et. al

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X