Was bedeutet T && (doppeltes kaufmännisches Und) in C ++ 11?

799

Ich habe mir einige der neuen Funktionen von C ++ 11 angesehen und eine, die mir aufgefallen ist, ist das doppelte kaufmännische Und beim Deklarieren von Variablen, wie z T&& var.

Wie heißt dieses Biest? Ich wünschte, Google würde uns erlauben, nach solchen Interpunktionen zu suchen.

Was genau bedeutet das?

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

paxdiablo
quelle
55
Ich habe dies zur c ++ - FAQ hinzugefügt, da ich sicher bin, dass es in Zukunft noch mehr geben wird.
GManNickG
3
verwandte Frage über Bewegungssemantik
Fredoverflow
41
Sie können mit Google danach suchen. Sie müssen Ihre Phrase nur in Anführungszeichen setzen: google.com/#q="T%26%26 "hat jetzt Ihre Frage als ersten Treffer. :)
sbi
Es gibt eine sehr gute, leicht verständliche Antwort auf eine ähnliche Frage hier stackoverflow.com/questions/7153991/…
Daniel
2
Ich habe oben drei Stackoverflow-Fragen, die in Google nach "c ++ two ampersands parameter" suchen, und Ihre war die erste. Sie müssen hierfür nicht einmal Interpunktion verwenden, wenn Sie "zwei kaufmännische Und-Parameter" buchstabieren können.
Sergiol

Antworten:

668

Es deklariert eine Wertreferenz (Standardvorschlagsdokument).

Hier ist eine Einführung in rvalue- Referenzen .

Hier ist ein fantastischer in tiefem Einblick in rvalue Referenzen von einer Microsoft-Standard - Bibliothek Entwicklern .

VORSICHT: Der verlinkte Artikel zu MSDN ("R-Wert-Referenzen: C ++ 0x-Funktionen in VC10, Teil 2") ist eine sehr klare Einführung in R-Wert-Referenzen, enthält jedoch Aussagen zu R-Wert-Referenzen, die im Entwurf von C ++ 11 einmal zutrafen Standard, aber nicht wahr für den letzten! Insbesondere heißt es an verschiedenen Stellen, dass r-Wert-Referenzen an l-Werte binden können, was einmal wahr war, aber geändert wurde (z. B. int x; int & rrx = x; wird nicht mehr in GCC kompiliert) - drawbarbs 13. Juli 14 um 16:12

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

T&& r = T();

rWertreferenzen sehen in erster Linie Folgendes vor:

Semantik verschieben . Es kann jetzt ein Verschiebungskonstruktor und ein Verschiebungszuweisungsoperator definiert werden, die anstelle der üblichen const-l-Wertreferenz eine r-Wert-Referenz verwenden. Ein Umzug funktioniert wie eine Kopie, außer dass er nicht verpflichtet ist, die Quelle unverändert zu lassen. Tatsächlich ändert es normalerweise die Quelle so, dass es die verschobenen Ressourcen nicht mehr besitzt. Dies ist ideal, um überflüssige Kopien zu entfernen, insbesondere bei Standardbibliotheksimplementierungen.

Ein Kopierkonstruktor könnte beispielsweise folgendermaßen 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 dieser Konstruktor als temporär übergeben würde, wäre die Kopie nicht erforderlich, da wir wissen, dass das temporäre Element nur zerstört wird. Warum nicht die Ressourcen nutzen, die die temporäre bereits zugewiesen hat? 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 Verschiebungskonstruktor ändert tatsächlich sein Argument. Dies würde das Temporäre effektiv in das zu konstruierende Objekt "verschieben", wodurch die unnötige Kopie eliminiert würde.

Der Verschiebungskonstruktor wird für temporäre und nicht konstante Wertreferenzen verwendet, die mithilfe der std::moveFunktion explizit in Wertreferenzen konvertiert werden (er führt nur die Konvertierung durch). Der folgende Code ruft den Verschiebungskonstruktor für f1und auf 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 . Mit rvalue-Referenzen können wir Argumente für Vorlagenfunktionen ordnungsgemäß weiterleiten. Nehmen Sie zum Beispiel diese Werksfunktion:

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

Wenn wir aufrufen factory<foo>(5), wird das Argument abgeleitet int&, das nicht an ein Literal 5 gebunden ist, selbst wenn fooder Konstruktor ein nimmt int. Nun, wir könnten stattdessen verwenden A1 const&, aber was ist, wenn foodas Konstruktorargument als nicht konstante Referenz verwendet wird? Um eine wirklich allgemeine Fabrik Funktion zu machen, müssten wir , um eine Überlastung Fabrik auf A1&und auf A1 const&. Das mag in Ordnung sein, wenn werkseitig 1 Parametertyp verwendet wird, aber jeder zusätzliche Parametertyp würde die erforderliche Überlast mit 2 multiplizieren. Das ist sehr schnell nicht zu warten.

rvalue-Referenzen beheben dieses Problem, indem die Standardbibliothek eine std::forwardFunktion definieren kann, mit der lvalue / rvalue-Referenzen ordnungsgemäß weitergeleitet werden können. Weitere Informationen zur Funktionsweise std::forwardfinden Sie in dieser hervorragenden Antwort .

Dies ermöglicht es uns, die Factory-Funktion folgendermaßen 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 der Wert / Wert des Arguments erhalten, wenn er an den TKonstruktor übergeben wird. Das heißt, wenn die Factory mit einem r-Wert aufgerufen wird, wird Tder Konstruktor mit einem r-Wert aufgerufen. Wenn factory mit einem lvalue aufgerufen wird, wird Tder Konstruktor mit einem lvalue aufgerufen. Die verbesserte Werksfunktion funktioniert aufgrund einer speziellen Regel:

Wenn der Funktionsparametertyp die Form hat, T&&in der Tes sich um einen Vorlagenparameter handelt, und das Funktionsargument ein Wert vom Typ ist A, wird der Typ A&für die Ableitung von Vorlagenargumenten verwendet.

So können wir Fabrik wie folgt verwenden:

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

Wichtige rWertreferenz-Eigenschaften :

  • Für die Überlastungsauflösung bevorzugen l-Werte die Bindung an l-Wert-Referenzen und r-Werte die Bindung an r-Wert-Referenzen . Daher bevorzugen Temporäre den Aufruf eines Verschiebungskonstruktors / Verschiebungszuweisungsoperators gegenüber einem Kopierkonstruktor / Zuweisungsoperator.
  • rWertreferenzen werden implizit an rWerte und an temporäre Werte gebunden, die das Ergebnis einer impliziten Konvertierung sind . dh float f = 0f; int&& i = f;ist gut geformt, weil float implizit in int konvertierbar ist; Der Verweis würde sich auf ein temporäres Element beziehen, das das Ergebnis der Konvertierung ist.
  • Benannte rWertreferenzen sind lWerte. Unbenannte rWertreferenzen sind rWerte. Dies ist wichtig, um zu verstehen, warum der std::moveAnruf erforderlich ist in:foo&& r = foo(); foo f = std::move(r);
Peter Huene
quelle
65
+1 für Named rvalue references are lvalues. Unnamed rvalue references are rvalues.; Ohne dies zu wissen, habe ich mich bemüht zu verstehen, warum Menschen T &&t; std::move(t);lange Zeit in Bewegungsabläufen arbeiten und dergleichen.
Legends2k
@MaximYegorushkin: In diesem Beispiel ist r an einen reinen r-Wert (temporär) gebunden, und daher sollte der Lebensdauerbereich des temporären r erweitert werden, nicht wahr?
Peter Huene
@PeterHuene Ich nehme das zurück, eine R-Wert-Referenz verlängert die Lebensdauer eines temporären.
Maxim Egorushkin
32
VORSICHT : Der verlinkte Artikel zu MSDN ("R-Wert-Referenzen: C ++ 0x-Funktionen in VC10, Teil 2") ist eine sehr klare Einführung in R-Wert-Referenzen, enthält jedoch Aussagen zu R-Wert-Referenzen, die im Entwurf von C ++ 11 einmal zutrafen Standard, aber nicht wahr für den letzten! Insbesondere heißt es an verschiedenen Stellen, dass r-Wert-Referenzen an l-Werte binden können, was einmal wahr war, aber geändert wurde (z. B. int x; int &&rrx = x; nicht mehr in GCC kompiliert )
drawbarbs
@PeterHuene Ist im obigen Beispiel nicht typename identity<T>::type& agleichbedeutend mit T&?
ibp73
81

Es bezeichnet eine Wertreferenz. R-Wert-Referenzen werden nur an temporäre Objekte gebunden, sofern nicht ausdrücklich anders generiert. Sie werden verwendet, um Objekte unter bestimmten Umständen wesentlich effizienter zu gestalten und eine Funktion bereitzustellen, die als perfekte Weiterleitung bezeichnet wird und den Vorlagencode erheblich vereinfacht.

In C ++ 03 können Sie nicht zwischen einer Kopie eines nicht veränderlichen l-Werts und einem r-Wert 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 Wertesemantik beizubehalten, was eine neue Heap-Zuordnung beinhaltet. Im zweiten Fall wissen wir jedoch im Voraus, dass das Objekt, das an unseren Konstruktor übergeben wurde, sofort zerstört werden muss und nicht unberührt bleiben muss. In diesem Szenario, das wesentlich effizienter ist, können wir effektiv nur die internen Zeiger austauschen und überhaupt kein Kopieren durchführen. Die Verschiebungssemantik kommt jeder Klasse zugute, die das Kopieren von intern referenzierten Ressourcen teuer oder verboten hat. Betrachten Sie den Fall von std::unique_ptr- jetzt, da unsere Klasse zwischen temporären und nicht temporären unterscheiden kann, können wir die Verschiebungssemantik korrekt ausführen, sodass die unique_ptrnicht kopiert, sondern verschoben werden kann, was bedeutet, dassstd::unique_ptrkann legal in Standardcontainern gespeichert, sortiert usw. werden, C ++ 03 std::auto_ptrnicht.

Nun betrachten wir die andere Verwendung von rWert-Referenzen - perfekte Weiterleitung. Betrachten Sie die Frage der Bindung eines Verweises an einen Verweis.

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 rWert-Referenzen kritisch. Eine r-Wert-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 Sie die einfachste Vorlagenfunktion min und max. In C ++ 03 müssen Sie alle vier Kombinationen von const und non-const manuell überladen. In C ++ 0x ist es nur eine Überladung. In Kombination mit verschiedenen 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 den Abzug vom Rückgabetyp weggelassen, weil ich mich nicht erinnern kann, wie es ohne weiteres gemacht wurde, aber diese min kann jede Kombination von l-Werten, r-Werten und konstanten Werten akzeptieren.

Hündchen
quelle
warum hast du benutzt std::forward<A>(aref) < std::forward<B>(bref)? und ich glaube nicht, dass diese Definition korrekt sein wird, wenn Sie vorwärts int&und versuchen float&. Legen Sie besser eine Formularvorlage ab.
Yankes
25

Der Begriff für die T&& Verwendung mit Typabzug (z. B. für die perfekte Weiterleitung) wird umgangssprachlich als Weiterleitungsreferenz bezeichnet . Der Begriff "universelle Referenz" 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 Referenzen

mmocny
quelle
14

Eine r-Wert-Referenz ist ein Typ, der sich mit mehreren Ausnahmen ähnlich wie die normale Referenz X & verhält. Das Wichtigste ist, dass lvalues ​​bei der Auflösung von Funktionsüberlastungen alte lvalue-Referenzen 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 l-Wert ist ein Ausdruck, der sich auf einen Speicherort bezieht und es uns ermöglicht, die Adresse dieses Speicherorts über den Operator & zu ermitteln.

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

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

Der Konstruktor und die Zuweisungsoperatoren wurden mit Versionen überladen, die rWertreferenzen enthalten. Mit R-Wert-Referenzen kann eine Funktion zur Kompilierungszeit (über die Überlastungsauflösung) unter der Bedingung "Wird ich für einen l-Wert oder einen r-Wert aufgerufen?" Verzweigen. Dies ermöglichte es uns, effizientere Konstruktor- und Zuweisungsoperatoren darüber zu erstellen, die Ressourcen verschieben, anstatt 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 aus, ob der Verschiebungskonstruktor oder der Verschiebungszuweisungsoperator aufgerufen werden soll.

Zusammenfassend lässt sich sagen, dass rWertreferenzen eine Verschiebungssemantik ermöglichen (und eine perfekte Weiterleitung, die im folgenden Artikellink beschrieben 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 Verschiebungskonstruktoren:

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 durchgefü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 erklärt (z. B. wie rvalues ​​eine perfekte Weiterleitung ermöglichen und was dies bedeutet), mit vielen guten Beispielen, ist Thomas Beckers 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. al

kurt krueckeberg
quelle
Ist das nicht so, dass der Kopierkonstruktor Sample(const Sample& s)auch den Inhalt kopieren muss? Die gleiche Frage für den 'Kopierzuweisungsoperator'.
K. Karamazen
Ja, du hast recht. Ich konnte den Speicher nicht kopieren. Der Kopierkonstruktor und der Kopierzuweisungsoperator sollten beide memcpy (ptr, s.ptr, size) ausführen, nachdem diese Größe getestet wurde! = 0. Und der Standardkonstruktor sollte memset (ptr, 0, size) ausführen, wenn size! = 0.
kurt krueckeberg
Okay danke. Somit können dieser Kommentar und die beiden vorherigen Kommentare entfernt werden, da das Problem auch in der Antwort behoben wurde.
K. Karamazen