web-dev-qa-db-de.com

Was bedeutet T && (doppeltes Et-Zeichen) in C ++ 11?

Ich habe mir einige der neuen Funktionen von C++ 11 angesehen und festgestellt, dass beim Deklarieren von Variablen doppeltes kaufmännisches Und-Zeichen verwendet wird, z. B. T&& var.

Wie heißt dieses Biest? Ich wünschte, Google würde es uns erlauben, nach Interpunktionszeichen wie diesem zu suchen.

Was genau bedeutet es Mittelwert?

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

725
paxdiablo

Es deklariert eine rWert-Referenz (Standardvorschlagsdokument).

Hier ist eine Einführung in rvalue Referenzen .

Hier finden Sie einen fantastischen Einblick in die Wertereferenzen einer der Microsoft-Standardbibliotheken Entwickler .

ACHTUNG: der verlinkte Artikel auf MSDN ("Rvalue References: C++ 0x Features in VC10, Part 2") ist eine sehr klare Einführung in Rvalue-Referenzen, gibt jedoch Aussagen über Rvalue-Referenzen ab, die einst im Entwurf des C++ 11-Standards zutrafen, aber für den endgültigen nicht zutreffen! Konkret heißt es an verschiedenen Stellen, dass rWert-Referenzen an lWerte binden können, was einst wahr war, aber geändert wurde (z. B. int x; int & rrx = x; wird nicht mehr in GCC kompiliert) - drewbarbs 13.07.14 um 16:12 Uhr

Der größte Unterschied zwischen einer C++ 03-Referenz (in C++ 11 jetzt als lvalue-Referenz bezeichnet) besteht darin, dass sie an einen Wert wie eine temporäre gebunden werden kann, ohne dass sie const sein muss. Somit ist diese Syntax jetzt legal:

T&& r = T();

rWertreferenzen sehen in erster Linie Folgendes vor:

Semantik verschieben . Es können nun ein Verschiebungskonstruktor und ein Verschiebungszuweisungsoperator definiert werden, die eine r-Wert-Referenz anstelle der üblichen const-l-Wert-Referenz verwenden. Ein Umzug funktioniert wie eine Kopie, es sei denn, er ist nicht verpflichtet, die Quelle unverändert zu lassen. In der Regel wird die Quelle so geändert, dass die verschobenen Ressourcen nicht mehr im Besitz der Quelle sind. Dies ist ideal, um unnötige Kopien zu vermeiden, insbesondere bei Standard-Bibliotheksimplementierungen.

Ein Kopierkonstruktor könnte beispielsweise so aussehen:

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

Wenn diesem Konstruktor ein temporäres Objekt übergeben würde, wäre die Kopie nicht erforderlich, da wir wissen, dass das temporäre Objekt nur zerstört wird. warum nicht die bereits zugewiesenen Ressourcen des Provisoriums nutzen? In C++ 03 gibt es keine Möglichkeit, die Kopie zu verhindern, da wir nicht feststellen können, dass uns eine temporäre Kopie übergeben wurde. In C++ 11 können wir einen Verschiebungskonstruktor überladen:

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

Beachten Sie hier den großen Unterschied: Der move-Konstruktor ändert tatsächlich sein Argument. Dies würde das temporäre Objekt effektiv in das zu erstellende Objekt "verschieben", wodurch die unnötige Kopie eliminiert wird.

Der move-Konstruktor wird für temporäre und nicht konstante Wertereferenzen verwendet, die explizit mit der Funktion std::move in r-Wertereferenzen konvertiert werden (die Konvertierung wird nur ausgeführt). Der folgende Code ruft den Verschiebungskonstruktor für f1 und f2 auf:

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 . Mit rWert-Referenzen können Argumente für Vorlagenfunktionen ordnungsgemäß weitergeleitet werden. Nehmen Sie zum Beispiel diese Factory-Funktion:

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

Wenn wir factory<foo>(5) aufrufen, wird das Argument als int& abgeleitet, was nicht an ein Literal 5 gebunden wird, selbst wenn der Konstruktor von foo ein int annimmt. Nun, wir könnten stattdessen A1 const& verwenden, aber was ist, wenn foo das Konstruktorargument als Nicht-Konstanten-Referenz verwendet? Um eine wirklich generische Factory-Funktion zu erstellen, müssten wir die Factory auf A1& und auf A1 const& überladen. Das mag in Ordnung sein, wenn die Fabrik einen Parametertyp annimmt, aber jeder zusätzliche Parametertyp würde die notwendige Überlastung mit 2 multiplizieren. Das ist sehr schnell nicht mehr zu erreichen.

rvalue-Referenzen beheben dieses Problem, indem die Standardbibliothek eine std::forward -Funktion definieren kann, die lvalue/rvalue-Referenzen ordnungsgemäß weiterleiten kann. Weitere Informationen zur Funktionsweise von std::forward finden Sie unter diese ausgezeichnete Antwort .

Dadurch können wir die Factory-Funktion folgendermaßen 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 der Wert/die Wertigkeit des Arguments erhalten, wenn es an den Konstruktor von T übergeben wird. Das heißt, wenn factory mit einem r-Wert aufgerufen wird, wird der Konstruktor von T mit einem r-Wert aufgerufen. Wenn factory mit einem lvalue aufgerufen wird, wird der Konstruktor von T mit einem lvalue aufgerufen. Die verbesserte Factory-Funktion funktioniert aufgrund einer speziellen Regel:

Wenn der Funktionsparametertyp die Form T&& hat, wobei T ein Vorlagenparameter ist und das Funktionsargument ein Wert vom Typ A ist, wird der Typ A& verwendet Template Argument Abzug.

So können wir Fabrik so verwenden:

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

Wichtige rWert-Referenzeigenschaften :

  • Für eine Überladungsauflösung bevorzugen l-Werte die Bindung an l-Wertereferenzen und r-Werte die Bindung an r-Wertereferenzen . Daher ziehen Temporäre es vor, einen Verschiebungskonstruktor/Verschiebungszuweisungsoperator gegenüber einem Kopierkonstruktor/Zuweisungsoperator aufzurufen.
  • rWertreferenzen binden implizit an rWerte und an temporäre Werte, die das Ergebnis einer impliziten Konvertierung sind . d.h. float f = 0f; int&& i = f; ist gut geformt, weil float implizit in int konvertierbar ist; Der Verweis wäre auf eine temporäre Datei, die das Ergebnis der Konvertierung ist.
  • Benannte rWert-Referenzen sind lWerte. Unbenannte rWert-Referenzen sind rWerte. Dies ist wichtig, um zu verstehen, warum der std::move -Aufruf erforderlich ist in: foo&& r = foo(); foo f = std::move(r);
624
Peter Huene

Es bezeichnet eine rWertreferenz. Rvalue-Referenzen werden nur an temporäre Objekte gebunden, sofern nicht ausdrücklich etwas anderes generiert wurde. Sie werden verwendet, um Objekte unter bestimmten Umständen effizienter zu gestalten und eine als perfekte Weiterleitung bekannte Funktion bereitzustellen, die den Vorlagencode erheblich vereinfacht.

In C++ 03 können Sie nicht zwischen einer Kopie eines nicht veränderbaren l-Werts und eines r-Werts 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 Zeichenfolge eine Kopie ausführen, um die Wertsemantik beizubehalten, was eine neue Heap-Zuordnung erfordert. Im zweiten Fall wissen wir jedoch im Voraus, dass das Objekt, das an unseren Konstrukteur übergeben wurde, sofort zur Zerstörung fällig ist und nicht unberührt bleiben muss. In diesem wesentlich effizienteren Szenario können wir effektiv nur die internen Zeiger austauschen und überhaupt nicht kopieren. Die Verschiebungssemantik kommt jeder Klasse zugute, die das Kopieren intern referenzierter Ressourcen teuer oder verboten hat. Stellen Sie sich den Fall von std::unique_ptr vor. Jetzt, da unsere Klasse zwischen temporären und nicht temporären Elementen unterscheiden kann, kann die Verschiebungssemantik korrekt funktionieren, sodass der unique_ptr nicht kopiert, sondern verschoben werden kann std::unique_ptr kann legal in Standardcontainern gespeichert, sortiert usw. werden, während dies in C++ 03 mit std::auto_ptr nicht möglich ist.

Nun betrachten wir die andere Verwendung von rWertreferenzen - perfekte Weiterleitung. Betrachten Sie die Frage, ob eine Referenz an eine Referenz gebunden werden soll.

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 R-Wert-Referenzen kritisch. Eine r-Wert-Referenz auf einen Typ T, wobei T ein Referenztyp ist, wird zu einer Referenz des Typs 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 Sie die einfachste Vorlagenfunktion - 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 variablen 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 auf den Rückgabetyp-Abzug verzichtet, weil ich mich nicht erinnern kann, wie es spontan gemacht wurde, aber das min kann jede Kombination von lWerten, rWerten, konst. LWerten akzeptieren.

79
Puppy

Der Begriff für T&& bei Verwendung mit Typabzug (z. B. für die perfekte Weiterleitung) wird umgangssprachlich als Weiterleitungsreferenz bezeichnet. ). Der Begriff "Universalreferenz" wurde von Scott Meyers in diesem Artikel geprägt, aber später geändert.

Das liegt daran, dass es entweder ein r-Wert oder ein l-Wert sein 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 Verweise

25
mmocny

Eine rWert-Referenz ist ein Typ, der sich mit einigen Ausnahmen ähnlich wie die normale Referenz X & verhält. Das Wichtigste ist, dass lvalues ​​bei der Auflösung von Funktionsüberlastungen die lvalue-Referenzen alten Stils 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 ein Wert? Alles, was kein Wert ist. Ein Wert ist ein Ausdruck, der sich auf einen Speicherort bezieht und es uns ermöglicht, die Adresse dieses Speicherorts über den Operator & abzurufen.

Es ist fast einfacher zu verstehen, was Werte mit einem Beispiel bewirken:

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

Die Konstruktor- und Zuweisungsoperatoren wurden mit Versionen überladen, die rWert-Referenzen enthalten. Rvalue-Referenzen ermöglichen die Verzweigung einer Funktion zur Kompilierungszeit (über eine Überlastungsauflösung) unter der Bedingung "Bin ich auf einen lvalue oder einen rvalue angewiesen?". Dadurch konnten wir effizientere Konstruktor- und Zuweisungsoperatoren erstellen, die Ressourcen verschieben, statt sie zu kopieren.

Der Compiler verzweigt automatisch zur Kompilierungszeit (abhängig davon, ob er für einen l-Wert oder einen r-Wert aufgerufen wird) und wählt, ob der Verschiebungskonstruktor oder der Verschiebungszuweisungsoperator aufgerufen werden sollen.

Zusammenfassend lässt sich sagen, dass rWert-Referenzen eine Verschiebungssemantik ermöglichen (und eine perfekte Weiterleitung, die im folgenden Artikel-Link erläutert wird).

Ein praktisches, leicht verständliches Beispiel ist die Klassenvorlage std :: unique_ptr . Da ein unique_ptr das ausschließliche Eigentum an seinem zugrunde liegenden Rohzeiger behält, können unique_ptrs nicht kopiert werden. Das würde ihre Invariante des ausschließlichen Eigentums verletzen. Sie haben also 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) wird normalerweise mit std :: move ausgeführt

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

Ein ausgezeichneter Artikel, der all dies und mehr (wie Werte eine perfekte Weiterleitung ermöglichen und was das bedeutet) mit vielen guten Beispielen erklärt, ist Thomas Becker's C++ Rvalue References Explained . Dieser Beitrag stützte sich stark auf seinen Artikel.

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

11