524 Stimmen

C++11 rvalues und move-Semantik Verwirrung (return-Anweisung)

Ich versuche, rvalue-Referenzen und die Move-Semantik von C++11 zu verstehen.

Was ist der Unterschied zwischen diesen Beispielen, und wer 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();

671voto

Howard Hinnant Punkte 191035

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 einen temporären Wert zurück, der von rval_ref . Die Geltungsdauer dieses Zeitraums wird über das rval_ref Definition und Sie können es so verwenden, als hätten Sie es nach Wert gefangen. Dies ist dem Folgenden sehr ähnlich:

const std::vector<int>& rval_ref = return_vector();

mit der Ausnahme, dass man in meiner Neufassung offensichtlich nicht verwenden kann rval_ref auf eine nicht-konstante 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 nun einen Verweis auf die 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 en tmp ist unnötig und kann sogar zu einer Leistungsverschlechterung führen, da sie die Optimierung der Rückgabewerte behindert.

Der beste Weg, das zu kodieren, was Sie tun, ist:

Bewährte Verfahren

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

D.h. genauso wie in C++03. tmp wird in der Return-Anweisung implizit als r-Wert behandelt. Er wird entweder mittels Return-Value-Optimierung zurückgegeben (kein Kopieren, kein Verschieben), oder wenn der Compiler entscheidet, dass er kein RVO durchführen kann, dann wird er verwendet den Konstruktor von vector move, um die Rückgabe durchzuführen . Nur wenn RVO nicht durchgeführt wird und wenn der zurückgegebene Typ keinen Move-Konstruktor hat, würde der Copy-Konstruktor für die Rückgabe verwendet werden.

48voto

Puppy Punkte 141483

Keiner von ihnen wird kopiert, aber der zweite wird auf einen zerstörten Vektor verweisen. Benannte rvalue-Referenzen kommen in normalem Code fast nie vor. Sie schreiben ihn 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();

Nur ist der Vektor jetzt verschoben. Die Benutzer einer Klasse hat in den meisten Fällen nichts mit ihren rvalue-Referenzen zu tun.

18voto

Zoner Punkte 596

Die einfache Antwort ist, dass Sie Code für rvalue-Referenzen wie normalen Referenzcode schreiben sollten, und Sie sollten sie in 99 % der Fälle mental gleich behandeln. Dies schließt alle alten Regeln über die Rückgabe von Referenzen ein (d.h. niemals eine Referenz auf eine lokale Variable zurückgeben).

Es sei denn, Sie schreiben eine Template-Containerklasse, die die Vorteile von std::forward nutzen muss und in der Lage ist, eine generische Funktion zu schreiben, die entweder lvalue- oder rvalue-Referenzen annimmt, ist dies mehr oder weniger wahr.

Einer der großen Vorteile des move-Konstruktors und der move-Zuweisung ist, dass der Compiler sie in Fällen verwenden kann, in denen die RVO (Rückgabewertoptimierung) und NRVO (benannte Rückgabewertoptimierung) nicht aufgerufen werden können, wenn Sie sie definieren. Dies ist sehr wichtig für die effiziente Rückgabe von teuren Objekten wie Containern und Strings aus Methoden.

Interessant wird es bei rvalue-Referenzen, weil man sie auch als Argumente für normale Funktionen verwenden kann. Dies ermöglicht es Ihnen, Container zu schreiben, die Überladungen sowohl für const-Referenzen (const foo& other) als auch für rvalue-Referenzen (foo&& other) haben. Selbst wenn das Argument zu unhandlich ist, um es mit einem einfachen Konstruktoraufruf zu übergeben, kann man es trotzdem tun:

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 aktualisiert, um Move-Überladungen für fast alles zu haben (Hash-Schlüssel und -Werte, Vektoreinfügung usw.), und hier werden Sie sie am häufigsten sehen.

Man kann sie auch für normale Funktionen verwenden, und wenn man nur ein rvalue-Referenzargument angibt, kann man den Aufrufer dazu zwingen, das Objekt zu erstellen und die Funktion die Verschiebung vornehmen zu lassen. Dies ist eher ein Beispiel als eine wirklich gute Anwendung, aber in meiner Rendering-Bibliothek habe ich allen geladenen Ressourcen eine Zeichenkette zugewiesen, so dass es einfacher ist, im Debugger zu sehen, was jedes Objekt darstellt. Die Schnittstelle sieht in etwa so aus:

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 handelt sich um eine Form der "undichten Abstraktion", die es mir jedoch ermöglicht, die Tatsache zu nutzen, dass ich die Zeichenfolge in den meisten Fällen bereits erstellen musste, und zu vermeiden, dass sie noch einmal kopiert wird. Dies ist nicht gerade ein hochleistungsfähiger Code, aber ein gutes Beispiel für die Möglichkeiten, die sich ergeben, wenn man sich mit dieser Funktion vertraut gemacht hat. Dieser Code erfordert eigentlich, dass die Variable entweder ein temporäres Element für den Aufruf ist oder std::move aufgerufen wird:

// move from temporary
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));

o

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

o

// 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 lässt sich nicht kompilieren!

string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);

4voto

Red XIII Punkte 5465

Keine Antwort per se sondern eine Richtschnur. Meistens ist es nicht sehr sinnvoll, die lokalen T&& Variable (wie Sie es mit std::vector<int>&& rval_ref ). Sie müssen trotzdem std::move() sie zur Verwendung in foo(T&&) Typ-Methoden. Es gibt auch das bereits erwähnte Problem, dass, wenn Sie versuchen, solche rval_ref von der Funktion erhalten Sie den Standardverweis-auf-zerstörtes-zeitgenössisches-Fiasko.

Die meiste Zeit würde ich nach folgendem Muster vorgehen:

// Declarations
A a(B&&, C&&);
B b();
C c();

auto ret = a(b(), c());

Sie halten keine Verweise auf zurückgegebene temporäre Objekte und vermeiden so den Fehler eines (unerfahrenen) Programmierers, der ein verschobenes Objekt verwenden möchte.

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

Natürlich gibt es (wenn auch eher selten) Fälle, in denen eine Funktion wirklich eine T&& was ein Verweis auf eine nicht temporär Objekt, das Sie in Ihr Objekt verschieben können.

Zu RVO: Diese Mechanismen funktionieren im Allgemeinen und der Compiler kann das Kopieren gut vermeiden, aber in Fällen, in denen der Rückgabepfad nicht offensichtlich ist (Ausnahmen, if Conditionals, die das benannte Objekt bestimmen, das Sie zurückgeben werden, und wahrscheinlich einige andere) sind rrefs Ihre Retter (auch wenn sie potenziell teurer sind).

3voto

Edward Strange Punkte 39597

Keiner von ihnen wird zusätzliche Kopien anfertigen. Selbst wenn RVO nicht verwendet wird, besagt die neue Norm, dass bei Rückgaben die Konstruktion von Verschiebungen dem Kopieren vorzuziehen ist, glaube ich.

Ich glaube, dass Ihr zweites Beispiel undefiniertes Verhalten verursacht aber, weil Sie einen Verweis auf eine lokale Variable zurückgeben.

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