C++11 rvalues ​​und Verwirrung der Bewegungssemantik (Rückgabeanweisung)

Lesezeit: 12 Minuten

C11 rvalues ​​und Verwirrung der Bewegungssemantik Ruckgabeanweisung
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();

  • 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 verschoben std::move(expression).

    – fredoverflow

    23. Dezember 2013 um 8:22 Uhr


1647127214 881 C11 rvalues ​​und Verwirrung der Bewegungssemantik Ruckgabeanweisung
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.

  • Compiler werden RVO, wenn Sie ein lokales Objekt nach Wert zurückgeben, und der Typ des Lokalen und die Rückgabe der Funktion identisch sind und keiner von beiden cv-qualifiziert ist (keine konstanten Typen zurückgeben). Vermeiden Sie die Rückkehr mit der Bedingung (:?), da dies RVO hemmen kann. Schließen Sie das Local nicht in eine andere Funktion ein, die einen Verweis auf das Local zurückgibt. Nur return my_local;. Mehrere return-Anweisungen sind in Ordnung und werden RVO nicht hemmen.

    – Howard Hinnant

    25. Februar 2013 um 20:18 Uhr

  • Es gibt eine Einschränkung: Bei der Rückgabe a Mitglied eines lokalen Objekts muss die Verschiebung explizit sein.

    – Knabenhaft

    26. Februar 2013 um 8:58 Uhr

  • @NoSenseEtAl: In der Rückleitung wird kein Temporär erstellt. move erstellt kein temporäres. Es wandelt einen lvalue in einen xvalue um, erstellt keine Kopien, erstellt nichts, zerstört nichts. Dieses Beispiel ist genau die gleiche Situation, als ob Sie per Lvalue-Referenz zurückgegeben und die entfernt hätten move aus der Rückgabezeile: In beiden Fällen haben Sie einen baumelnden Verweis auf eine lokale Variable innerhalb der Funktion, die zerstört wurde.

    – Howard Hinnant

    27. Februar 2013 um 16:11 Uhr

  • “Mehrere Rückgabeanweisungen sind ok und werden RVO nicht hemmen”: Nur wenn sie zurückkehren das Gleiche Variable.

    – Deduplizierer

    29. Juli 2014 um 22:17 Uhr

  • @Deduplikator: Sie haben Recht. Ich habe nicht so genau gesprochen, wie ich beabsichtigt hatte. Ich meinte, dass mehrere Rückgabeanweisungen den Compiler nicht von RVO abhalten (obwohl dies die Implementierung unmöglich macht), und daher wird der Rückgabeausdruck immer noch als rvalue betrachtet.

    – Howard Hinnant

    29. Juli 2014 um 22:21 Uhr

1647127214 274 C11 rvalues ​​und Verwirrung der Bewegungssemantik Ruckgabeanweisung
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.

  • Sind Sie wirklich sicher, dass das dritte Beispiel eine Vektorkopie erstellen wird?

    – Tarantel

    13. Februar 2011 um 20:41 Uhr

  • @Tarantula: Es wird deinen Vektor sprengen. Ob es es vor dem Bruch kopiert hat oder nicht, spielt keine Rolle.

    – Hündchen

    13. Februar 2011 um 20:50 Uhr

  • Ich sehe keinen Grund für die von Ihnen vorgeschlagene Vernichtung. Es ist völlig 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

    13. Februar 2011 um 22:09 Uhr

  • Nur ein Punkt zur Klarstellung, da ich das lerne. In diesem neuen Beispiel ist der Vektor tmp ist nicht gerührt hinein rval_refsondern direkt in geschrieben rval_ref unter Verwendung von RVO (dh Kopieren Elision). Es wird unterschieden zwischen std::move und Kopieren Elision. EIN std::move kann noch einige zu kopierende Daten beinhalten; im Fall eines Vektors wird tatsächlich ein neuer Vektor im Kopierkonstruktor konstruiert und Daten werden zugewiesen, aber der Großteil des Datenarrays wird nur kopiert, indem der Zeiger (im Wesentlichen) kopiert wird. Der Kopierverzicht vermeidet 100% aller Kopien.

    – Mark Lakata

    11. Dezember 2014 um 1:26 Uhr

  • @MarkLakata Das ist NRVO, nicht RVO. NRVO ist optional, sogar in C++17. Wenn es nicht angewendet wird, geben sowohl den Wert als auch zurück rval_ref Variablen werden mit dem Move-Konstruktor von konstruiert std::vector. Sowohl mit als auch ohne ist kein Kopierkonstruktor beteiligt std::move. tmp wird als behandelt rwert in return Aussage in diesem Fall.

    – Daniel Langr

    20. Februar 2018 um 13:13 Uhr


C11 rvalues ​​und Verwirrung der Bewegungssemantik Ruckgabeanweisung
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.

1647127215 275 C11 rvalues ​​und Verwirrung der Bewegungssemantik Ruckgabeanweisung
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 Bauch 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 Cder Compiler hat diese Informationen von std::move()weswegen struct A bekommt gerührtes sei denn, die Instanz von class C ist konstant.

995380cookie-checkC++11 rvalues ​​und Verwirrung der Bewegungssemantik (Rückgabeanweisung)

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

Privacy policy