Rvalue-Referenzen verstehen

Lesezeit: 9 Minuten

Ich glaube, es gibt etwas, das ich bei rvalue-Referenzen nicht ganz verstehe. Warum kann Folgendes nicht kompiliert werden (VS2012) mit dem Fehler 'foo' : cannot convert parameter 1 from 'int' to 'int &&'?

void foo(int &&) {}
void bar(int &&x) { foo(x); };

Ich hätte angenommen, dass der Typ int && würde erhalten bleiben, wenn von bar nach foo übergegangen würde. Warum verwandelt es sich in int einmal innerhalb des Funktionskörpers?

Ich weiß, die Antwort ist zu verwenden std::forward:

void bar(int &&x) { foo(std::forward<int>(x)); }

also habe ich vielleicht einfach kein klares verständnis warum. (Auch warum nicht std::move?)

  • stackoverflow.com/questions/6864880/…

    – Timrau

    26. September 2012 um 16:53 Uhr

  • Du verwirrst Ausdrücke, Variablen und Verweise.

    – Kerrek SB

    26. September 2012 um 17:41 Uhr


Benutzer-Avatar
SaulBack

Ich erinnere mich immer an lvalue als einen Wert, der einen Namen hat oder angesprochen werden kann. Da x einen Namen hat, wird er als Lvalue übergeben. Der Zweck des Verweises auf rvalue besteht darin, der Funktion zu ermöglichen, den Wert auf jede Art und Weise, die sie für richtig hält, vollständig zu überschreiben. Wenn wir wie in Ihrem Beispiel x als Referenz übergeben, können wir nicht wissen, ob dies sicher ist:

void foo(int &&) {}
void bar(int &&x) { 
    foo(x); 
    x.DoSomething();   // what could x be?
};

Tun foo(std::move(x)); teilt dem Compiler ausdrücklich mit, dass Sie mit x fertig sind und es nicht mehr benötigen. Ohne diesen Schritt könnten schlimme Dinge mit bestehendem Code passieren. Das std::move ist eine Absicherung.

std::forward wird für die perfekte Weiterleitung in Vorlagen verwendet.

  • Ich mag deine Erklärung. Der Teil “dem Compiler sagen, dass wir fertig sind” ließ die Teile in meinem Kopf besser zusammenpassen.

    – Moswald

    26. September 2012 um 23:06 Uhr

Warum verwandelt es sich in int einmal innerhalb des Funktionskörpers?

Das tut es nicht; es ist immer noch ein Verweis auf ein rwert.

Wenn ein Name in einem Ausdruck erscheint, ist es ein Wert – auch wenn es sich um einen Verweis auf eine handelt rwert. Es kann in ein umgewandelt werden rwert wenn der Ausdruck dies erfordert (dh wenn sein Wert benötigt wird); aber es kann nicht an gebunden werden rwert Hinweis.

Also, wie Sie sagen, um es an ein anderes zu binden rwert Verweis, müssen Sie ihn explizit in einen unbenannten konvertieren rwert. std::forward und std::move sind bequeme Möglichkeiten, dies zu tun.

Auch, warum nicht std::move?

Warum eigentlich nicht? Das wäre sinnvoller als std::forwarddie für Vorlagen gedacht ist, die nicht wissen, ob das Argument eine Referenz ist.

Benutzer-Avatar
David Schwarz

Es ist das “keine Namensregel“. Innen bar, x hat einen Namen … x. Also ist es jetzt ein lvalue. Wenn Sie etwas als Rvalue-Referenz an eine Funktion übergeben, wird es nicht zu einem Rvalue innerhalb der Funktion.

Wenn Sie nicht sehen, warum es so sein muss, fragen Sie sich – was ist x nach foo kehrt zurück? (Denken Sie daran, foo ist frei beweglich x.)

  • Die Art von x ist immer noch eine Rvalue-Referenz, weil es so deklariert ist, aber der Ausdruck ist ein Lvalue. Ich würde einfach “macht es nicht zu einer Rvalue-Referenz” in “macht es nicht zu einer Rvalue” ändern.

    – Josef Mansfeld

    26. September 2012 um 17:09 Uhr

  • @sftrabbit: Danke. Das ist besser.

    – David Schwartz

    26. September 2012 um 17:11 Uhr

Benutzer-Avatar
Oliv

rwert und Wert sind Kategorien von Ausdrücke.

rvalue-Referenz und Lvalue-Referenz sind Kategorien von Verweise.

Innerhalb einer Deklaration, T x&& = <initializer expression>die Variable x hat den Typ T&& und kann an einen Ausdruck (das ) gebunden werden, der ein ist rvalue-Ausdruck. Daher wurde T&& benannt rvalue-Referenztypweil es sich auf ein bezieht rvalue-Ausdruck.

Innerhalb einer Deklaration, T x& = <initializer expression>, die Variable x hat den Typ T& und kann an einen Ausdruck (das ) gebunden werden, der ein lvalue-Ausdruck (++) ist. Daher wurde T& benannt lvalue-Referenztypweil es sich auf ein beziehen kann lvalue-Ausdruck.

Daher ist es in C++ wichtig, zwischen der Benennung einer Entität, die in einer Deklaration erscheint, und der Benennung dieses Namens in einem Ausdruck zu unterscheiden.

Wenn ein Name in einem Ausdruck wie in erscheint foo(x)der Name x allein ist ein Ausdruck, der als ID-Ausdruck bezeichnet wird. Per Definition ist ein ID-Ausdruck immer ein lvalue-Ausdruck und ein Lvalue-Ausdrücke kann nicht an eine gebunden werden rvalue-Referenz.

Wenn es um Rvalue-Referenzen geht, ist es wichtig, zwischen zwei wichtigen, nicht zusammenhängenden Schritten in der Lebensdauer einer Referenz zu unterscheiden – Bindung und Wertesemantik.

Bindung bezieht sich hier auf die genaue Art und Weise, wie ein Wert beim Aufrufen einer Funktion mit dem Parametertyp abgeglichen wird.

Wenn Sie beispielsweise die Funktionsüberladungen haben:

void foo(int a) {}
void foo(int&& a) {}

Dann beim Anruf foo(x)umfasst die Auswahl der richtigen Überladung das Binden des Werts x zum Parameter von foo.

Bei rvalue-Referenzen geht es nur um Bindungssemantik.

In den Körpern beider foo-Funktionen befindet sich die Variable a fungiert als regulärer lvalue. Das heißt, wenn wir die zweite Funktion wie folgt umschreiben:

void foo(int&& a) {
    foo(a);
} 

dann sollte dies intuitiv zu einem Stapelüberlauf führen. Aber das ist nicht der Fall – bei rvalue-Referenzen geht es nur um die Bindung und niemals um die Semantik von Werten. Seit a ein regulärer lvalue innerhalb des Funktionskörpers ist, dann die erste Überladung foo(int) wird an diesem Punkt aufgerufen und es tritt kein Stapelüberlauf auf. Ein Stapelüberlauf würde nur auftreten, wenn wir den Werttyp von explizit ändern azB durch Verwendung std::move:

void foo(int&& a) {
    foo(std::move(a));
}

An dieser Stelle kommt es aufgrund der geänderten Wertsemantik zu einem Stapelüberlauf.

Dies ist meiner Meinung nach das verwirrendste Merkmal von Rvalue-Referenzen – dass der Typ während und nach der Bindung unterschiedlich funktioniert. Es ist eine Rvalue-Referenz beim Binden, aber danach verhält es sich wie eine Lvalue-Referenz. Eine Variable vom Typ rvalue-Referenz verhält sich in jeder Hinsicht wie eine Variable vom Typ lvalue-Referenz, nachdem die Bindung erfolgt ist.

Der einzige Unterschied zwischen einer lvalue- und einer rvalue-Referenz besteht beim Binden – wenn sowohl eine lvalue- als auch eine rvalue-Überladung verfügbar ist, werden temporäre Objekte (oder besser gesagt xvalues ​​- eXpiring-Werte) vorzugsweise an rvalue-Referenzen gebunden:

void goo(const int& x) {}
void goo(int&& x) {}

goo(5); // this will call goo(int&&) because 5 is an xvalue

Das ist der einzige Unterschied. Technisch gesehen hindert Sie nichts daran, Rvalue-Referenzen wie Lvalue-Referenzen zu verwenden, außer der Konvention:

void doit(int&& x) {
    x = 123;
}

int a;
doit(std::move(a));
std::cout << a; // totally valid, prints 123, but please don't do that

Und das Stichwort hier ist „Konvention“. Da rvalue-Referenzen vorzugsweise an temporäre Objekte gebunden werden, ist es vernünftig anzunehmen, dass Sie das temporäre Objekt ausnehmen können, dh alle seine Daten von ihm entfernen können, da es nach dem Aufruf in keiner Weise zugänglich ist und sowieso zerstört wird :

std::vector<std::string> strings;
string.push_back(std::string("abc"));

Im obigen Ausschnitt das temporäre Objekt std::string("abc") kann in keiner Weise nach der Anweisung verwendet werden, in der es erscheint, da es an keine Variable gebunden ist. Deswegen push_back darf seinen Inhalt wegbewegen, anstatt ihn zu kopieren, und sich daher eine zusätzliche Zuweisung und Freigabe ersparen.

Das heißt, es sei denn, Sie verwenden std::move:

std::vector<std::string> strings;
std::string mystr("abc");
string.push_back(std::move(mystr));

Jetzt das Objekt mystr ist nach dem Anruf noch erreichbar push_backaber push_back weiß das nicht – es geht immer noch davon aus, dass es das Objekt ausnehmen darf, weil es als Rvalue-Referenz übergeben wird. Aus diesem Grund ist das Verhalten von std::move() ist eine Konvention und auch warum std::move() an sich macht eigentlich nichts – insbesondere macht es keine Bewegung. Es markiert sein Argument nur als “bereit, ausgeweidet zu werden”.


Der letzte Punkt ist: Rvalue-Referenzen sind nur nützlich, wenn sie zusammen mit Lvalue-Referenzen verwendet werden. Es gibt keinen Fall, in dem ein Rvalue-Argument an sich nützlich ist (hier übertreiben).

Angenommen, Sie haben eine Funktion, die eine Zeichenfolge akzeptiert:

void foo(std::string);

Wenn die Funktion die Zeichenfolge einfach untersuchen und keine Kopie davon erstellen soll, verwenden Sie const&:

void foo(const std::string&);

Dies vermeidet immer eine Kopie beim Aufruf der Funktion.

Wenn die Funktion eine Kopie der Zeichenfolge ändern oder speichern soll, verwenden Sie die Wertübergabe:

void foo(std::string s);

In diesem Fall erhalten Sie eine Kopie, wenn der Aufrufer einen lvalue übergibt, und temporäre Objekte werden direkt erstellt, wodurch eine Kopie vermieden wird. Dann benutze std::move(s) wenn Sie den Wert von s speichern wollen, zB in einer Member-Variablen. Beachten Sie, dass dies auch dann effizient funktioniert, wenn der Aufrufer eine Rvalue-Referenz übergibt foo(std::move(mystring)); Weil std::string bietet einen Bewegungskonstruktor.

Die Verwendung eines rvalue hier ist eine schlechte Wahl:

void foo(std::string&&)

weil es dem Aufrufer die Last auferlegt, das Objekt vorzubereiten. Insbesondere wenn der Aufrufer eine Kopie eines Strings an diese Funktion übergeben möchte, muss er dies explizit tun;

std::string s;
foo(s); // XXX: doesn't compile
foo(std::string(s)); // have to create copy manually

Und wenn Sie eine veränderliche Referenz an eine Variable übergeben möchten, verwenden Sie einfach eine reguläre lvalue-Referenz:

void foo(std::string&);

Die Verwendung von rvalue-Referenzen ist in diesem Fall technisch möglich, aber semantisch falsch und völlig verwirrend.

Der einzige Ort, an dem ein Rvalue-Verweis sinnvoll ist, ist in einem Move-Konstruktor oder Move-Zuweisungsoperator. In allen anderen Situationen sind Pass-by-Value- oder Lvalue-Referenzen normalerweise die richtige Wahl und vermeiden viel Verwirrung.


Hinweis: Verwechseln Sie Rvalue-Referenzen nicht mit Weiterleitungsreferenzen, die genau gleich aussehen, aber völlig anders funktionieren, wie in:

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

Im obigen Beispiel t sieht aus wie ein rvalue-Referenzparameter, ist aber eigentlich eine Weiterleitungsreferenz (aufgrund des Vorlagentyps), was eine ganz andere Wurmkiste ist.

1019000cookie-checkRvalue-Referenzen verstehen

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy