C ++ 11 rWerte und Verschieben der Semantikverwirrung (return-Anweisung)

435

Ich versuche, rWert-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 machen?

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();
Tarantel
quelle
50
Bitte geben Sie niemals lokale Variablen als Referenz zurück. Eine rWertreferenz ist immer noch eine Referenz.
Fredoverflow
63
Das war offensichtlich beabsichtigt, um die semantischen Unterschiede zwischen den Beispielen lol
Tarantula
@FredOverflow Alte Frage, aber ich habe eine Sekunde gebraucht, um Ihren Kommentar zu verstehen. Ich denke, die Frage mit # 2 war, ob std::move()eine dauerhafte "Kopie" erstellt wurde.
3Dave
5
@DavidLively std::move(expression)erstellt nichts, sondern wandelt den Ausdruck einfach in einen x-Wert um. Während der Auswertung werden keine Objekte kopiert oder verschoben std::move(expression).
Fredoverflow

Antworten:

562

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 eine temporäre Datei zurück, die von abgefangen wird rval_ref. Dieses temporäre Leben wird über die rval_refDefinition hinaus verlängert, und Sie können es so verwenden, als hätten Sie es nach Wert erfasst. Dies ist dem folgenden sehr ähnlich:

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

außer dass Sie in meinem Umschreiben offensichtlich nicht rval_refin einer nicht konstanten Weise verwenden können.

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 erstellt. rval_refenthält nun einen Verweis auf das tmpinnerhalb der Funktion zerstörte. 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::moveauf tmpist unnötig und kann tatsächlich eine Leistung Pessimierung sein , wie es Rückgabewert Optimierung hemmen.

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

Dh genau wie in C ++ 03. tmpwird in der return-Anweisung implizit als r-Wert behandelt. Es wird entweder über die Rückgabewertoptimierung (keine Kopie, keine Verschiebung) zurückgegeben, oder wenn der Compiler entscheidet, dass RVO nicht ausgeführt werden kann, wird dies getan verwendet er den Verschiebungskonstruktor des Vektors, um die Rückgabe . Nur wenn RVO nicht ausgeführt wird und der zurückgegebene Typ keinen Verschiebungskonstruktor hat, wird der Kopierkonstruktor für die Rückgabe verwendet.

Howard Hinnant
quelle
64
Compiler werden RVO, wenn Sie ein lokales Objekt nach Wert zurückgeben, und der Typ der lokalen und die Rückgabe der Funktion sind gleich, und keiner ist cv-qualifiziert (keine konstanten Typen zurückgeben). Vermeiden Sie es, mit der Bedingung (:?) Zurückzukehren, da dies die RVO hemmen kann. Schließen Sie das Local nicht in eine andere Funktion ein, die einen Verweis auf das Local zurückgibt. Einfach return my_local;. Mehrere Rückgabeanweisungen sind in Ordnung und hemmen RVO nicht.
Howard Hinnant
27
Es gibt eine Einschränkung: Wenn Sie ein Mitglied eines lokalen Objekts zurückgeben, muss die Verschiebung explizit erfolgen.
Boycy
5
@NoSenseEtAl: In der Rückleitung wird keine temporäre Datei erstellt. moveerstellt keine temporäre. Es wandelt einen l-Wert in einen x-Wert um, erstellt keine Kopien, erstellt nichts und zerstört nichts. Dieses Beispiel ist genau die gleiche Situation, als ob Sie per lvalue-reference zurückgegeben und die moveaus der Rückgabezeile entfernt hätten: In beiden Fällen haben Sie eine baumelnde Referenz auf eine lokale Variable innerhalb der Funktion, die zerstört wurde.
Howard Hinnant
15
"Mehrere return-Anweisungen sind in Ordnung und verhindern RVO nicht": Nur wenn sie dieselbe Variable zurückgeben.
Deduplikator
5
@Deduplicator: Sie sind richtig. Ich sprach nicht so genau, wie ich es beabsichtigt hatte. Ich meinte, dass mehrere return-Anweisungen dem Compiler RVO nicht verbieten (obwohl dies die Implementierung unmöglich macht), und daher wird der return-Ausdruck immer noch als rvalue betrachtet.
Howard Hinnant
42

Keiner von ihnen wird kopiert, aber der zweite bezieht sich auf einen zerstörten Vektor. Benannte rWertreferenzen existieren im regulären Code fast nie. Sie schreiben es so, 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. Der Benutzer einer Klasse befasst sich in den allermeisten Fällen nicht mit ihren Wertreferenzen.

Hündchen
quelle
Sind Sie wirklich sicher, dass das dritte Beispiel eine Vektorkopie durchführen wird?
Tarantula
@ Tarantula: Es wird Ihren Vektor sprengen. Es spielt keine Rolle, ob es vor dem Brechen kopiert wurde oder nicht.
Welpe
4
Ich sehe keinen Grund für die von Ihnen vorgeschlagene Pleite. Es ist vollkommen in Ordnung, eine lokale rvalue-Referenzvariable an einen rvalue zu binden. In diesem Fall wird die Lebensdauer des temporären Objekts auf die Lebensdauer der Referenzvariablen rvalue verlängert.
Fredoverflow
1
Nur ein Punkt zur Klarstellung, da ich das lerne. In diesem neuen Beispiel der Vektor tmpwird nicht bewegt in rval_ref, sondern direkt in geschrieben rval_refmit RVO (dh kopieren elision). Es gibt einen Unterschied zwischen std::moveund Kopierentscheidung. A enthält std::movemöglicherweise noch einige zu kopierende Daten. Im Fall eines Vektors wird tatsächlich ein neuer Vektor im Kopierkonstruktor erstellt und Daten werden zugewiesen, aber der Großteil des Datenarrays wird nur durch Kopieren des Zeigers (im Wesentlichen) kopiert. Die Kopierentfernung vermeidet 100% aller Kopien.
Mark Lakata
@ MarkLakata Dies ist NRVO, nicht RVO. NRVO ist auch in C ++ 17 optional. Wenn es nicht angewendet wird, werden sowohl der Rückgabewert als auch die rval_refVariablen mit dem Verschiebungskonstruktor von erstellt std::vector. Es ist kein Kopierkonstruktor mit / ohne beteiligt std::move. tmpwird in diesem Fall in der Anweisung als rWert behandelt return.
Daniel Langr
16

Die einfache Antwort ist, dass Sie Code für r-Wert-Referenzen schreiben sollten, wie Sie regulären Referenzcode verwenden würden, und dass Sie sie 99% der Zeit mental gleich behandeln sollten. Dies schließt alle alten Regeln zum Zurückgeben von Referenzen ein (dh niemals einen Verweis auf eine lokale Variable zurückgeben).

Sofern Sie keine Vorlagencontainerklasse schreiben, die std :: forward nutzen und eine generische Funktion schreiben muss, die entweder lvalue- oder rvalue-Referenzen verwendet, ist dies mehr oder weniger richtig.

Einer der großen Vorteile des Verschiebungskonstruktors und der Verschiebungszuweisung besteht darin, dass der Compiler sie verwenden kann, wenn Sie sie definieren, wenn die RVO (Rückgabewertoptimierung) und NRVO (benannte Rückgabewertoptimierung) nicht aufgerufen werden. Dies ist ziemlich umfangreich, um teure Objekte wie Container und Strings wertmäßig effizient aus Methoden zurückzugeben.

Wenn rvalue-Referenzen interessant werden, können Sie sie auch als Argumente für normale Funktionen verwenden. Auf diese Weise können Sie Container schreiben, die sowohl für die const-Referenz (const foo & other) als auch für die rvalue-Referenz (foo && other) überladen sind. Selbst wenn das Argument zu unhandlich ist, um mit einem bloßen Konstruktoraufruf übergeben zu werden, kann es dennoch getan 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 aktualisiert, um Verschiebungsüberladungen für fast alles (Hash-Schlüssel und -Werte, Vektoreinfügung usw.) zu haben, und dort werden Sie sie am häufigsten sehen.

Sie können sie auch für normale Funktionen verwenden. Wenn Sie nur ein rvalue-Referenzargument angeben, können Sie den Aufrufer zwingen, das Objekt zu erstellen und die Funktion die Verschiebung ausführen zu 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 Sie leichter sehen können, was jedes Objekt im Debugger darstellt. Die Schnittstelle ist ungefähr 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", aber ich kann die Tatsache ausnutzen, dass ich die Zeichenfolge bereits die meiste Zeit erstellen musste, und vermeiden, sie erneut zu kopieren. Dies ist nicht gerade ein Hochleistungscode, aber ein gutes Beispiel für die Möglichkeiten, wenn die Leute den Dreh raus haben. Dieser Code erfordert, 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 kompiliert!

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

Keine Antwort an sich , sondern eine Richtlinie. Meistens macht es wenig Sinn, lokale T&&Variablen zu deklarieren (wie Sie es getan haben std::vector<int>&& rval_ref). Sie müssen sie weiterhin std::move()in foo(T&&)Typmethoden verwenden. Es gibt auch das Problem, das bereits erwähnt wurde, wenn Sie versuchen, solche zurückzugebenrval_ref Funktion zurückzugeben, den Standardverweis auf das zerstörte temporäre Fiasko erhalten.

Die meiste Zeit würde ich mit folgendem Muster gehen:

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

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

Sie haben keine Verweise auf zurückgegebene temporäre Objekte und vermeiden so (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&&was eine Referenz auf ein nicht temporäres Objekt ist, 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, ifBedingungen, die das benannte Objekt bestimmen, das Sie zurückgeben werden, und wahrscheinlich andere koppeln), sind Rrefs Ihre Retter (auch wenn möglicherweise mehr teuer).

Rot XIII
quelle
2

Keiner von diesen wird extra kopieren. Selbst wenn RVO nicht verwendet wird, besagt der neue Standard, dass die Verschiebungskonstruktion bei Rückgaben meiner Meinung nach dem Kopieren vorgezogen wird.

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

Edward Strange
quelle
1

Wie bereits in den Kommentaren zur ersten Antwort erwähnt, kann das return std::move(...);Konstrukt 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 ist dies return std::move(some_member);nur dann sinnvoll, wenn Sie das bestimmte Klassenmitglied tatsächlich verschieben möchten, z. B. in einem Fall, in dem class Ckurzlebige Adapterobjekte mit dem alleinigen Zweck dargestellt werden, Instanzen von zu erstellenstruct A .

Beachten Sie, wie struct Aimmer kopiert wird class B, auch wenn das class BObjekt ein R-Wert ist. Dies liegt daran, dass der Compiler nicht erkennen kann, dass class Bdie Instanz von struct Anicht mehr verwendet wird. In class Chat der Compiler diese Informationen von std::move(), weshalb struct Abekommt bewegt , es sei denn , die Instanz von class Ckonstant ist.

Andrej Podzimek
quelle