it-swarm.com.de

C ++ 11-Werte und Verschiebungssemantikverwirrung (return-Anweisung)

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

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 Objekt zurück, das von rval_ref Abgefangen wird. Dieses temporäre Objekt hat eine Lebensdauer, die über die rval_ref - Definition hinausgeht, und Sie können es so verwenden, als hätten Sie es anhand des Werts abgefangen. Dies ist dem Folgenden sehr ähnlich:

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

mit der Ausnahme, dass Sie in meinem Rewrite rval_ref offensichtlich nicht in 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_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. Das std::move Für tmp ist nicht erforderlich und kann eine Leistungspessimierung darstellen, da es die Rückgabewertoptimierung 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();

Das heißt genauso wie in C++ 03. tmp wird in der return-Anweisung implizit als r-Wert behandelt. Es wird entweder durch Rückgabewertoptimierung (keine Kopie, keine Verschiebung) zurückgegeben, oder wenn der Compiler entscheidet, dass RVO nicht ausgeführt werden kann, wird es verwendet den Verschiebungskonstruktor von vector, um die Rückgabe durchzuführen . Nur wenn RVO nicht ausgeführt wird und der zurückgegebene Typ keinen move-Konstruktor hat, wird der copy-Konstruktor für die Rückgabe verwendet.

536
Howard Hinnant

Keiner von ihnen wird kopieren, aber der zweite wird sich auf einen zerstörten Vektor beziehen. Benannte rvalue-Referenzen existieren im regulären Code so gut wie nie. Sie schreiben es so, wie Sie es 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. Das ser einer Klasse behandelt in den allermeisten Fällen nicht ihre Wertereferenzen.

41
Puppy

Die einfache Antwort ist, dass Sie Code für R-Wert-Referenzen schreiben sollten, wie Sie es mit normalem Referenz-Code tun würden, und dass Sie sie in 99% der Fälle mental gleich behandeln sollten. Dies schließt alle alten Regeln zum Zurückgeben von Referenzen ein (d. H. Niemals eine Referenz auf eine lokale Variable zurückgeben).

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

Einer der großen Vorteile für den Verschiebungskonstruktor und die Verschiebungszuweisung besteht darin, dass der Compiler sie verwenden kann, wenn RVO (Rückgabewertoptimierung) und NRVO (benannte Rückgabewertoptimierung) nicht aufgerufen werden. Dies ist ziemlich umfangreich, um teure Objekte wie Container und Strings nach Wert effizient aus Methoden zurückzugeben.

Das Interessante an rvalue-Referenzen ist, dass Sie sie auch als Argumente für normale Funktionen verwenden können. Auf diese Weise können Sie Container schreiben, die sowohl für die const-Referenz (const foo & other) als auch für die r-Wert-Referenz (foo && other) Überladungen aufweisen. Selbst wenn das Argument zu unhandlich ist, um mit einem reinen Konstruktoraufruf übergeben zu werden, kann es dennoch ausgefü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 für fast alles (Hash-Schlüssel und Werte, Vektoreinfügung usw.) Bewegungsüberladungen aufweisen. Dort werden sie am häufigsten angezeigt.

Sie können sie auch für normale Funktionen verwenden. Wenn Sie nur ein R-Wert-Referenzargument angeben, können Sie den Aufrufer zwingen, das Objekt zu erstellen und die Funktion die Bewegung ausführen zu lassen. Dies ist eher ein Beispiel als eine wirklich gute Verwendung, aber in meiner Renderingbibliothek habe ich allen geladenen Ressourcen eine Zeichenfolge zugewiesen, damit leichter erkennbar ist, was jedes Objekt im Debugger darstellt. Das Interface sieht ungefähr 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 ist eine Form einer "undichten Abstraktion", aber ich kann die Tatsache ausnutzen, dass ich die Zeichenfolge bereits die meiste Zeit erstellen musste, und es vermeiden, eine weitere Kopie davon anzufertigen. Dies ist nicht gerade ein Hochleistungscode, sondern ein gutes Beispiel für die Möglichkeiten, mit denen sich die Leute an diese Funktion gewöhnen können. Dieser Code erfordert tatsächlich, dass die Variable entweder eine temporäre Variable für den Aufruf ist oder dass 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);
16
Zoner

Keine Antwort per se, sondern eine Richtlinie. In den meisten Fällen ist es nicht sinnvoll, eine lokale T&& - Variable zu deklarieren (wie bei std::vector<int>&& rval_ref). Sie müssen sie noch std::move() verwenden, um sie in Methoden des Typs foo(T&&) zu verwenden. Es gibt auch das Problem, das bereits erwähnt wurde: Wenn Sie versuchen, ein solches rval_ref Von der Funktion zurückzugeben, erhalten Sie den Standardverweis auf ein zerstörtes temporäres Fiasko.

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

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

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

Sie haben keine Referenzen für zurückgegebene temporäre Objekte, sodass Sie den (unerfahrenen) Programmiererfehler vermeiden, 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();

Offensichtlich gibt es (obwohl eher selten) Fälle, in denen eine Funktion wirklich einen T&& Zurückgibt, der auf ein nicht temporäres Objekt verweist, das Sie in Ihr Objekt verschieben können.

In Bezug auf RVO: Diese Mechanismen funktionieren im Allgemeinen, und der Compiler kann das Kopieren auf angenehme Weise vermeiden. In Fällen, in denen der Rückgabepfad nicht offensichtlich ist (Ausnahmen, if Bedingungen, die das zurückgegebene benannte Objekt bestimmen, und wahrscheinlich auch einige andere), gehören die rrefs zu Ihnen Retter (auch wenn möglicherweise teurer).

3
Red XIII

Keiner von diesen wird extra kopieren. Selbst wenn RVO nicht verwendet wird, sagt der neue Standard, dass die Bewegungskonstruktion bei Retouren, wie ich glaube, der Kopie vorgezogen wird.

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

2
Edward Strange

Wie bereits in den Kommentaren zur ersten Antwort erwähnt, kann das Konstrukt return std::move(...); in anderen Fällen als der Rückgabe lokaler Variablen einen Unterschied bewirken. Hier ist ein ausführbares Beispiel, das dokumentiert, was passiert, wenn Sie ein Mitgliedsobjekt mit und ohne std::move() zurückgeben:

#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 return std::move(some_member); nur sinnvoll, wenn Sie das bestimmte Klassenmitglied tatsächlich verschieben möchten, z. In einem Fall, in dem class C kurzlebige Adapterobjekte mit dem einzigen Zweck darstellt, Instanzen von struct A zu erstellen.

Beachten Sie, dass struct A Immer kopiert aus class B Erhält, auch wenn das Objekt class B Ein R-Wert ist. Dies liegt daran, dass der Compiler nicht mehr erkennen kann, dass die Instanz von class B Von struct A Nicht mehr verwendet wird. In class C Hat der Compiler diese Informationen aus std::move(), weshalb struct Averschoben erhält, es sei denn, die Instanz von class C ist konstant.

0
Andrej Podzimek