
Tarantel
Ich versuche, rvalue-Referenzen zu verstehen und die Semantik von C++11 zu verschieben.
Was ist der Unterschied zwischen diesen Beispielen und welches von ihnen wird keine Vektorkopie erstellen?
Erstes Beispiel
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return tmp;
}
std::vector<int> &&rval_ref = return_vector();
Zweites Beispiel
std::vector<int>&& return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
Drittes Beispiel
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();

Howard Hinnant
Erstes Beispiel
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return tmp;
}
std::vector<int> &&rval_ref = return_vector();
Das erste Beispiel gibt ein temporäres zurück, das von abgefangen wird rval_ref
. Dieses Temporäre wird sein Leben über das hinaus verlängern rval_ref
Definition und Sie können es so verwenden, als ob Sie es nach Wert erfasst hätten. Dies ist dem Folgenden sehr ähnlich:
const std::vector<int>& rval_ref = return_vector();
außer dass Sie in meiner Umschreibung offensichtlich nicht verwenden können rval_ref
in nicht konstanter Weise.
Zweites Beispiel
std::vector<int>&& return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
Im zweiten Beispiel haben Sie einen Laufzeitfehler erzeugt. rval_ref
enthält jetzt einen Verweis auf das Zerstörte tmp
innerhalb der Funktion. Mit etwas Glück würde dieser Code sofort abstürzen.
Drittes Beispiel
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
Ihr drittes Beispiel entspricht in etwa Ihrem ersten. Die std::move
an tmp
ist unnötig und kann tatsächlich eine Performance-Pessimierung sein, da es die Optimierung des Rückgabewerts hemmt.
Der beste Weg, um zu codieren, was Sie tun, ist:
Beste Übung
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return tmp;
}
std::vector<int> rval_ref = return_vector();
Also genauso wie in C++03. tmp
wird in der return-Anweisung implizit als rvalue behandelt. Es wird entweder über die Rückgabewertoptimierung zurückgegeben (kein Kopieren, kein Verschieben), oder wenn der Compiler entscheidet, dass er RVO nicht ausführen kann, verwendet er den Verschiebekonstruktor von vector, um die Rückgabe durchzuführen. Nur wenn RVO nicht ausgeführt wird und der zurückgegebene Typ keinen Bewegungskonstruktor hat, wird der Kopierkonstruktor für die Rückgabe verwendet.

Hündchen
Keiner von ihnen wird kopieren, aber der zweite wird sich auf einen zerstörten Vektor beziehen. Benannte Rvalue-Referenzen existieren fast nie in normalem Code. Sie schreiben es genauso, wie Sie eine Kopie in C++03 geschrieben hätten.
std::vector<int> return_vector()
{
std::vector<int> tmp {1,2,3,4,5};
return tmp;
}
std::vector<int> rval_ref = return_vector();
Außer jetzt wird der Vektor verschoben. Die Benutzer einer Klasse befasst sich in den allermeisten Fällen nicht mit ihren Rvalue-Referenzen.

Zoner
Die einfache Antwort lautet: Sie sollten Code für rvalue-Referenzen wie normalen Referenzcode schreiben, und Sie sollten sie in 99 % der Fälle mental gleich behandeln. Dies schließt alle alten Regeln zur Rückgabe von Referenzen ein (dh niemals eine Referenz auf eine lokale Variable zurückgeben).
Dies gilt mehr oder weniger, es sei denn, Sie schreiben eine Container-Vorlagenklasse, die std::forward nutzen und in der Lage sein muss, eine generische Funktion zu schreiben, die entweder lvalue- oder rvalue-Referenzen verwendet.
Einer der großen Vorteile des Bewegungskonstruktors und der Bewegungszuweisung besteht darin, dass der Compiler sie verwenden kann, wenn Sie sie definieren, wenn RVO (Return Value Optimization) und NRVO (Named Return Value Optimization) nicht aufgerufen werden. Dies ist ziemlich groß, um teure Objekte wie Container und Zeichenfolgen nach Wert effizient von Methoden zurückzugeben.
Interessant wird es nun bei rvalue-Referenzen, dass Sie sie auch als Argumente für normale Funktionen verwenden können. Auf diese Weise können Sie Container schreiben, die Überladungen sowohl für die const-Referenz (const foo& other) als auch für die rvalue-Referenz (foo&& other) haben. Selbst wenn das Argument zu unhandlich ist, um es mit einem bloßen Konstruktoraufruf zu übergeben, kann es dennoch durchgeführt werden:
std::vector vec;
for(int x=0; x<10; ++x)
{
// automatically uses rvalue reference constructor if available
// because MyCheapType is an unamed temporary variable
vec.push_back(MyCheapType(0.f));
}
std::vector vec;
for(int x=0; x<10; ++x)
{
MyExpensiveType temp(1.0, 3.0);
temp.initSomeOtherFields(malloc(5000));
// old way, passed via const reference, expensive copy
vec.push_back(temp);
// new way, passed via rvalue reference, cheap move
// just don't use temp again, not difficult in a loop like this though . . .
vec.push_back(std::move(temp));
}
Die STL-Container wurden so aktualisiert, dass sie Move-Überladungen für fast alles haben (Hash-Schlüssel und -Werte, Einfügen von Vektoren usw.), und dort werden Sie sie am häufigsten sehen.
Sie können sie auch für normale Funktionen verwenden, und wenn Sie nur ein Rvalue-Referenzargument angeben, können Sie den Aufrufer zwingen, das Objekt zu erstellen, und die Funktion die Bewegung ausführen lassen. Dies ist eher ein Beispiel als eine wirklich gute Verwendung, aber in meiner Rendering-Bibliothek habe ich allen geladenen Ressourcen eine Zeichenfolge zugewiesen, damit es einfacher ist, zu sehen, was jedes Objekt im Debugger darstellt. Die Schnittstelle ist in etwa so:
TextureHandle CreateTexture(int width, int height, ETextureFormat fmt, string&& friendlyName)
{
std::unique_ptr<TextureObject> tex = D3DCreateTexture(width, height, fmt);
tex->friendlyName = std::move(friendlyName);
return tex;
}
Es ist eine Form einer ‘undichten Abstraktion’, erlaubt mir aber, die Tatsache auszunutzen, dass ich die Zeichenfolge die meiste Zeit bereits erstellen musste, und zu vermeiden, sie noch einmal zu kopieren. Dies ist nicht gerade Hochleistungscode, aber ein gutes Beispiel für die Möglichkeiten, wenn die Leute mit dieser Funktion den Dreh raus haben. Dieser Code erfordert tatsächlich, dass die Variable entweder temporär für den Aufruf ist oder std::move aufgerufen wird:
// move from temporary
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));
oder
// explicit move (not going to use the variable 'str' after the create call)
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, std::move(str));
oder
// explicitly make a copy and pass the temporary of the copy down
// since we need to use str again for some reason
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string(str));
aber das wird nicht kompilieren!
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);
Keine Antwort an sich, sondern ein Richtwert. Meistens macht es wenig Sinn, lokal zu deklarieren T&&
Variable (wie Sie es mit std::vector<int>&& rval_ref
). Das wirst du noch müssen std::move()
sie darin zu verwenden foo(T&&)
Typ Methoden. Es gibt auch das Problem, das bereits erwähnt wurde, dass, wenn Sie versuchen, eine solche zurückzugeben rval_ref
Von der Funktion erhalten Sie den Standardverweis auf das zerstörte temporäre Fiasko.
Meistens würde ich nach folgendem Schema vorgehen:
// Declarations
A a(B&&, C&&);
B b();
C c();
auto ret = a(b(), c());
Sie haben keine Verweise auf zurückgegebene temporäre Objekte, daher vermeiden Sie (unerfahrene) Programmiererfehler, die ein verschobenes Objekt verwenden möchten.
auto bRet = b();
auto cRet = c();
auto aRet = a(std::move(b), std::move(c));
// Either these just fail (assert/exception), or you won't get
// your expected results due to their clean state.
bRet.foo();
cRet.bar();
Offensichtlich gibt es (wenn auch eher seltene) Fälle, in denen eine Funktion wirklich a zurückgibt T&&
das ist ein Verweis auf a nicht vorübergehend Objekt, das Sie in Ihr Objekt verschieben können.
In Bezug auf RVO: Diese Mechanismen funktionieren im Allgemeinen und der Compiler kann das Kopieren gut vermeiden, aber in Fällen, in denen der Rückweg nicht offensichtlich ist (Ausnahmen, if
Bedingungen, die das benannte Objekt bestimmen, das Sie zurückgeben werden, und wahrscheinlich noch einige andere) rrefs sind Ihre Retter (auch wenn sie möglicherweise teurer sind).
Keiner von ihnen wird zusätzliche Kopien erstellen. Selbst wenn RVO nicht verwendet wird, sagt der neue Standard, dass die Zugkonstruktion meiner Meinung nach beim Kopieren dem Kopieren vorgezogen wird.
Ich glaube jedoch, dass Ihr zweites Beispiel undefiniertes Verhalten verursacht, weil Sie einen Verweis auf eine lokale Variable zurückgeben.

Andrej Podzimek
Wie bereits in den Kommentaren zur ersten Antwort erwähnt, die return std::move(...);
Konstrukt kann in anderen Fällen als der Rückgabe lokaler Variablen einen Unterschied machen. Hier ist ein ausführbares Beispiel, das dokumentiert, was passiert, wenn Sie ein Mitgliedsobjekt mit und ohne zurückgeben std::move()
:
#include <iostream>
#include <utility>
struct A {
A() = default;
A(const A&) { std::cout << "A copied\n"; }
A(A&&) { std::cout << "A moved\n"; }
};
class B {
A a;
public:
operator A() const & { std::cout << "B C-value: "; return a; }
operator A() & { std::cout << "B L-value: "; return a; }
operator A() && { std::cout << "B R-value: "; return a; }
};
class C {
A a;
public:
operator A() const & { std::cout << "C C-value: "; return std::move(a); }
operator A() & { std::cout << "C L-value: "; return std::move(a); }
operator A() && { std::cout << "C R-value: "; return std::move(a); }
};
int main() {
// Non-constant L-values
B b;
C c;
A{b}; // B L-value: A copied
A{c}; // C L-value: A moved
// R-values
A{B{}}; // B R-value: A copied
A{C{}}; // C R-value: A moved
// Constant L-values
const B bc;
const C cc;
A{bc}; // B C-value: A copied
A{cc}; // C C-value: A copied
return 0;
}
Vermutlich, return std::move(some_member);
nur dann sinnvoll, wenn Sie das jeweilige Klassenmitglied tatsächlich verschieben wollen, zB in einem Fall wo class C
repräsentiert kurzlebige Adapterobjekte mit dem einzigen Zweck, Instanzen von zu erstellen struct A
.
Beachte wie struct A
bekommt immer kopiert aus class B
auch wenn die class B
Objekt ist ein R-Wert. Dies liegt daran, dass der Compiler dies nicht erkennen kann class B
‘s Instanz von struct A
wird nicht mehr verwendet. Im class C
der Compiler hat diese Informationen von std::move()
weswegen struct A
bekommt gerührtes sei denn, die Instanz von class C
ist konstant.
9953800cookie-checkC++11 rvalues und Verwirrung der Bewegungssemantik (Rückgabeanweisung)yes
Bitte geben Sie niemals lokale Variablen als Referenz zurück. Eine Rvalue-Referenz ist immer noch eine Referenz.
– fredoverflow
13. Februar 2011 um 20:36 Uhr
Das war offensichtlich beabsichtigt, um die semantischen Unterschiede zwischen den Beispielen zu verstehen, lol
– Tarantel
15. Februar 2011 um 0:22 Uhr
@FredOverflow Alte Frage, aber ich habe eine Sekunde gebraucht, um Ihren Kommentar zu verstehen. Ich denke, die Frage mit # 2 war, ob
std::move()
erstellt eine dauerhafte “Kopie”.– 3Dave
22. Dezember 2013 um 23:17 Uhr
@DavidLively
std::move(expression)
erstellt nichts, es wandelt den Ausdruck einfach in einen xvalue um. Bei der Auswertung werden keine Objekte kopiert oder verschobenstd::move(expression)
.– fredoverflow
23. Dezember 2013 um 8:22 Uhr