Was ist Bewegungssemantik?

1702

Ich habe gerade das Podcast-Interview mit Scott Meyers über C ++ 0x gehört . Die meisten neuen Funktionen haben für mich Sinn gemacht, und ich freue mich jetzt tatsächlich auf C ++ 0x, mit einer Ausnahme. Ich bekomme immer noch keine Bewegungssemantik ... Was ist das genau?

Dicroce
quelle
20
Ich fand [Eli Benderskys Blog-Artikel] ( eli.thegreenplace.net/2011/12/15/… ) über l- und r-Werte in C und C ++ ziemlich informativ. Er erwähnt auch r-Wert-Referenzen in C ++ 11 und führt sie mit kleinen Beispielen ein.
Nils
16
Alex Allains Darstellung zu diesem Thema ist sehr gut geschrieben.
Patrick Sanan
19
Jedes Jahr frage ich mich, worum es bei der "neuen" Verschiebungssemantik in C ++ geht. Ich google sie und gehe auf diese Seite. Ich lese die Antworten, mein Gehirn schaltet sich aus. Ich gehe zurück zu C und vergesse alles! Ich bin festgefahren.
Himmel
7
@sky Betrachten Sie std :: vector <> ... Irgendwo darin befindet sich ein Zeiger auf ein Array auf dem Heap. Wenn Sie dieses Objekt kopieren, muss ein neuer Puffer zugewiesen und die Daten aus dem Puffer in den neuen Puffer kopiert werden. Gibt es einen Umstand, unter dem es in Ordnung wäre, einfach den Zeiger zu stehlen? Die Antwort lautet JA, wenn der Compiler weiß, dass das Objekt temporär ist. Mit der Verschiebungssemantik können Sie definieren, wie die Eingeweide Ihrer Klassen verschoben und in einem anderen Objekt abgelegt werden können, wenn der Compiler weiß, dass das Objekt, von dem Sie sich entfernen, bald verschwinden wird.
Dicroce
Die einzige Referenz, die ich verstehen kann: learncpp.com/cpp-tutorial/… , dh die ursprüngliche Begründung der Verschiebungssemantik stammt von intelligenten Zeigern.
jw_

Antworten:

2481

Ich finde es am einfachsten, die Bewegungssemantik mit Beispielcode zu verstehen. Beginnen wir mit einer sehr einfachen Zeichenfolgenklasse, die nur einen Zeiger auf einen Heap-zugewiesenen Speicherblock enthält:

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = std::strlen(p) + 1;
        data = new char[size];
        std::memcpy(data, p, size);
    }

Da wir die Erinnerung selbst verwalten gewählt haben, müssen wir die folgen Regeldetri . Ich werde das Schreiben des Zuweisungsoperators verschieben und vorerst nur den Destruktor und den Kopierkonstruktor implementieren:

    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = std::strlen(that.data) + 1;
        data = new char[size];
        std::memcpy(data, that.data, size);
    }

Der Kopierkonstruktor definiert, was das Kopieren von Zeichenfolgenobjekten bedeutet. Der Parameter ist const string& thatan alle Ausdrücke vom Typ string gebunden, mit denen Sie in den folgenden Beispielen Kopien erstellen können:

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

Jetzt kommt der Schlüsseleinblick in die Bewegungssemantik. Beachten Sie, dass nur in der ersten Zeile, in der wir kopieren, xdiese tiefe Kopie wirklich notwendig ist, da wir sie möglicherweise xspäter überprüfen möchten und sehr überrascht wären, wenn xsie sich irgendwie geändert hätte. Haben Sie bemerkt, dass ich gerade xdreimal gesagt habe (viermal, wenn Sie diesen Satz einfügen) und jedes Mal genau dasselbe Objekt gemeint habe ? Wir nennen Ausdrücke wie x"lWerte".

Die Argumente in den Zeilen 2 und 3 sind keine l-Werte, sondern r-Werte, da die zugrunde liegenden Zeichenfolgenobjekte keine Namen haben und der Client sie zu einem späteren Zeitpunkt nicht mehr überprüfen kann. r-Werte bezeichnen temporäre Objekte, die beim nächsten Semikolon zerstört werden (genauer gesagt: am Ende des vollständigen Ausdrucks, der den r-Wert lexikalisch enthält). Dies ist wichtig, da wir während der Initialisierung von bund cmit der Quellzeichenfolge tun konnten, was wir wollten, und der Client keinen Unterschied feststellen konnte !

C ++ 0x führt einen neuen Mechanismus namens "rvalue reference" ein, der es uns unter anderem ermöglicht, rvalue-Argumente durch Funktionsüberladung zu erkennen. Wir müssen lediglich einen Konstruktor mit einem rvalue-Referenzparameter schreiben. Innerhalb dieses Konstruktors können wir mit der Quelle alles tun, was wir wollen , solange wir sie in einem gültigen Zustand belassen:

    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

Was haben wir hier gemacht? Anstatt die Heap-Daten gründlich zu kopieren, haben wir nur den Zeiger kopiert und dann den ursprünglichen Zeiger auf null gesetzt (um zu verhindern, dass 'delete []' vom Destruktor des Quellobjekts unsere 'gerade gestohlenen Daten' freigibt). Tatsächlich haben wir die Daten "gestohlen", die ursprünglich zur Quellzeichenfolge gehörten. Die wichtigste Erkenntnis ist wiederum, dass der Client unter keinen Umständen feststellen konnte, dass die Quelle geändert wurde. Da wir hier nicht wirklich eine Kopie erstellen, nennen wir diesen Konstruktor einen "Verschiebungskonstruktor". Seine Aufgabe ist es, Ressourcen von einem Objekt zu einem anderen zu verschieben, anstatt sie zu kopieren.

Herzlichen Glückwunsch, Sie verstehen jetzt die Grundlagen der Bewegungssemantik! Fahren wir mit der Implementierung des Zuweisungsoperators fort. Wenn Sie mit dem Kopier- und Austausch-Idiom nicht vertraut sind , lernen Sie es und kehren Sie zurück, da es sich um ein fantastisches C ++ - Idiom handelt, das sich auf die Ausnahmesicherheit bezieht.

    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

Huh, das ist es? "Wo ist die rWertreferenz?" du könntest fragen. "Wir brauchen es hier nicht!" ist meine Antwort :)

Beachten Sie, dass wir den Parameter als that Wert übergeben und daher thatwie jedes andere Zeichenfolgenobjekt initialisiert werden müssen. Wie genau wird thates initialisiert? In den alten Tagen von C ++ 98 wäre die Antwort "vom Kopierkonstruktor" gewesen. In C ++ 0x wählt der Compiler zwischen dem Kopierkonstruktor und dem Verschiebungskonstruktor basierend darauf, ob das Argument für den Zuweisungsoperator ein l-Wert oder ein r-Wert ist.

Wenn Sie also sagen a = b, wird der Kopierkonstruktor initialisiert that(da der Ausdruck bein Wert ist), und der Zuweisungsoperator tauscht den Inhalt gegen eine frisch erstellte, tiefe Kopie aus. Das ist genau die Definition der Kopie und der Austauschsprache - erstellen Sie eine Kopie, tauschen Sie den Inhalt mit der Kopie aus und entfernen Sie die Kopie, indem Sie den Bereich verlassen. Hier gibt es nichts Neues.

Wenn Sie jedoch sagen a = x + y, wird der Verschiebungskonstruktor initialisiert that(da der Ausdruck x + yein r-Wert ist), sodass keine tiefe Kopie erforderlich ist, sondern nur eine effiziente Verschiebung. thatist immer noch ein unabhängiges Objekt vom Argument, aber seine Konstruktion war trivial, da die Heap-Daten nicht kopiert, sondern nur verschoben werden mussten. Es war nicht notwendig, es zu kopieren, da x + yes sich um einen r-Wert handelt. Auch hier ist es in Ordnung, von Zeichenfolgenobjekten zu wechseln, die mit r-Werten gekennzeichnet sind.

Zusammenfassend lässt sich sagen, dass der Kopierkonstruktor eine tiefe Kopie erstellt, da die Quelle unberührt bleiben muss. Der Verschiebungskonstruktor hingegen kann den Zeiger einfach kopieren und dann den Zeiger in der Quelle auf null setzen. Es ist in Ordnung, das Quellobjekt auf diese Weise "aufzuheben", da der Client keine Möglichkeit hat, das Objekt erneut zu untersuchen.

Ich hoffe, dieses Beispiel hat den Hauptpunkt vermittelt. Es gibt noch viel mehr, Referenzen zu bewerten und Semantik zu verschieben, was ich absichtlich weggelassen habe, um es einfach zu halten. Wenn Sie weitere Informationen wünschen, lesen Sie bitte meine ergänzende Antwort .

Fredoverflow
quelle
40
@Aber wenn mein ctor einen r-Wert erhält, der später nie mehr verwendet werden kann, warum muss ich mich dann überhaupt darum kümmern, ihn in einem konsistenten / sicheren Zustand zu belassen? Anstatt that.data = 0 zu setzen, lassen Sie es einfach stehen.
Einpoklum
70
@einpoklum Denn ohne that.data = 0würden die Charaktere viel zu früh (wenn der Temporäre stirbt) und auch zweimal zerstört. Sie möchten die Daten stehlen, nicht teilen!
Fredoverflow
19
@einpoklum Der regelmäßig geplante Destruktor wird weiterhin ausgeführt, daher müssen Sie sicherstellen, dass der Post-Move-Status des Quellobjekts keinen Absturz verursacht. Besser sollten Sie sicherstellen, dass das Quellobjekt auch der Empfänger einer Zuweisung oder eines anderen Schreibvorgangs sein kann.
CTMacUser
12
@pranitkothari Ja, alle Objekte müssen zerstört werden, auch von Objekten entfernt. Und da wir nicht möchten, dass das char-Array in diesem Fall gelöscht wird, müssen wir den Zeiger auf null setzen.
Fredoverflow
7
@ Virus721 delete[]auf einem Nullptr wird vom C ++ - Standard als No-Op definiert.
Fredoverflow
1057

Meine erste Antwort war eine extrem vereinfachte Einführung in die Verschiebungssemantik, und viele Details wurden absichtlich weggelassen, um sie einfach zu halten. Es gibt jedoch noch viel mehr, um die Semantik zu verschieben, und ich dachte, es wäre Zeit für eine zweite Antwort, um die Lücken zu füllen. Die erste Antwort ist schon ziemlich alt und es fühlte sich nicht richtig an, sie einfach durch einen völlig anderen Text zu ersetzen. Ich denke, es dient immer noch als erste Einführung. Aber wenn Sie tiefer graben wollen, lesen Sie weiter :)

Stephan T. Lavavej hat sich die Zeit genommen, wertvolles Feedback zu geben. Vielen Dank, Stephan!

Einführung

Durch die Verschiebungssemantik kann ein Objekt unter bestimmten Bedingungen die externen Ressourcen eines anderen Objekts übernehmen. Dies ist in zweierlei Hinsicht wichtig:

  1. Aus teuren Kopien billige Züge machen. Ein Beispiel finden Sie in meiner ersten Antwort. Beachten Sie, dass die Verschiebungssemantik keine Vorteile gegenüber der Kopiersemantik bietet, wenn ein Objekt nicht mindestens eine externe Ressource verwaltet (entweder direkt oder indirekt über seine Mitgliedsobjekte). In diesem Fall bedeutet das Kopieren eines Objekts und das Verschieben eines Objekts genau dasselbe:

    class cannot_benefit_from_move_semantics
    {
        int a;        // moving an int means copying an int
        float b;      // moving a float means copying a float
        double c;     // moving a double means copying a double
        char d[64];   // moving a char array means copying a char array
    
        // ...
    };
  2. Implementierung sicherer "Nur-Verschieben" -Typen; Das heißt, Typen, für die das Kopieren keinen Sinn macht, das Verschieben jedoch. Beispiele hierfür sind Sperren, Dateihandles und intelligente Zeiger mit eindeutiger Besitzersemantik. Hinweis: In dieser Antwort std::auto_ptrwird eine veraltete C ++ 98-Standardbibliotheksvorlage erläutert , die std::unique_ptrin C ++ 11 durch ersetzt wurde. Fortgeschrittene C ++ - Programmierer sind wahrscheinlich zumindest einigermaßen vertraut std::auto_ptr, und aufgrund der angezeigten "Verschiebungssemantik" scheint dies ein guter Ausgangspunkt für die Erörterung der Verschiebungssemantik in C ++ 11 zu sein. YMMV.

Was ist ein Zug?

Die C ++ 98-Standardbibliothek bietet einen intelligenten Zeiger mit einer eindeutigen Besitzersemantik namens std::auto_ptr<T>. Falls Sie nicht vertraut sind auto_ptr, soll damit sichergestellt werden, dass ein dynamisch zugewiesenes Objekt auch bei Ausnahmen immer freigegeben wird:

{
    std::auto_ptr<Shape> a(new Triangle);
    // ...
    // arbitrary code, could throw exceptions
    // ...
}   // <--- when a goes out of scope, the triangle is deleted automatically

Das Ungewöhnliche auto_ptrist das "Kopieren":

auto_ptr<Shape> a(new Triangle);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        |
        |
  +-----|---+
  |   +-|-+ |
a | p | | | |
  |   +---+ |
  +---------+

auto_ptr<Shape> b(a);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        +----------------------+
                               |
  +---------+            +-----|---+
  |   +---+ |            |   +-|-+ |
a | p |   | |          b | p | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+

Beachten Sie, wie die Initialisierung bmit asich nicht auf das Dreieck kopieren, sondern überträgt das Eigentum des Dreiecks aus azu b. Wir sagen auch " awird verschoben in b " oder "das Dreieck wird verschoben von a nach b ". Dies mag verwirrend klingen, da das Dreieck selbst immer an derselben Stelle im Speicher bleibt.

Ein Objekt zu verschieben bedeutet, den Besitz einer von ihm verwalteten Ressource auf ein anderes Objekt zu übertragen.

Der Kopierkonstruktor von auto_ptrsieht wahrscheinlich ungefähr so ​​aus (etwas vereinfacht):

auto_ptr(auto_ptr& source)   // note the missing const
{
    p = source.p;
    source.p = 0;   // now the source no longer owns the object
}

Gefährliche und harmlose Bewegungen

Das Gefährliche daran auto_ptrist, dass das, was syntaktisch wie eine Kopie aussieht, tatsächlich ein Zug ist. Wenn Sie versuchen, eine Mitgliedsfunktion für ein Verschieben von auto_ptraufzurufen, wird ein undefiniertes Verhalten ausgelöst. Sie müssen daher sehr vorsichtig sein, um keine zu verwenden, auto_ptrnachdem sie verschoben wurde von:

auto_ptr<Shape> a(new Triangle);   // create triangle
auto_ptr<Shape> b(a);              // move a into b
double area = a->area();           // undefined behavior

Ist auto_ptraber nicht immer gefährlich. Werksfunktionen sind ein perfekter Anwendungsfall für auto_ptr:

auto_ptr<Shape> make_triangle()
{
    return auto_ptr<Shape>(new Triangle);
}

auto_ptr<Shape> c(make_triangle());      // move temporary into c
double area = make_triangle()->area();   // perfectly safe

Beachten Sie, wie beide Beispiele demselben syntaktischen Muster folgen:

auto_ptr<Shape> variable(expression);
double area = expression->area();

Und doch ruft einer von ihnen undefiniertes Verhalten hervor, während der andere dies nicht tut. Was ist also der Unterschied zwischen den Ausdrücken aund make_triangle()? Sind sie nicht beide vom gleichen Typ? Sie sind es zwar, aber sie haben unterschiedliche Wertkategorien .

Wertekategorien

Offensichtlich muss es einen tiefgreifenden Unterschied zwischen dem Ausdruck, ader eine auto_ptrVariable bezeichnet, und dem Ausdruck geben, make_triangle()der den Aufruf einer Funktion bezeichnet, die einen auto_ptrby-Wert zurückgibt , wodurch bei auto_ptrjedem Aufruf ein neues temporäres Objekt erstellt wird. aist ein Beispiel für einen l-Wert , während make_triangle()es ein Beispiel für einen r-Wert ist .

Das Verschieben von Werten wie aist gefährlich, da wir später versuchen könnten, eine Mitgliedsfunktion über aaufzurufen und undefiniertes Verhalten aufzurufen. Auf der anderen Seite ist es absolut make_triangle()sicher, von r-Werten zu wechseln , da dies absolut sicher ist, nachdem der Kopierkonstruktor seine Arbeit erledigt hat. Es gibt keinen Ausdruck, der das Temporäre bezeichnet; Wenn wir einfach noch make_triangle()einmal schreiben , erhalten wir eine andere temporäre. Tatsächlich ist das vorübergehende Verschieben bereits in der nächsten Zeile verschwunden:

auto_ptr<Shape> c(make_triangle());
                                  ^ the moved-from temporary dies right here

Beachten Sie, dass die Buchstaben lund reinen historischen Ursprung auf der linken und rechten Seite einer Aufgabe haben. Dies gilt in C ++ nicht mehr, da es lWerte gibt, die nicht auf der linken Seite einer Zuweisung angezeigt werden können (wie Arrays oder benutzerdefinierte Typen ohne Zuweisungsoperator), und es gibt rWerte, die dies können (alle rWerte von Klassentypen) mit einem Zuweisungsoperator).

Ein rWert vom Klassentyp ist ein Ausdruck, dessen Auswertung ein temporäres Objekt erstellt. Unter normalen Umständen bezeichnet kein anderer Ausdruck innerhalb desselben Bereichs dasselbe temporäre Objekt.

RWertreferenzen

Wir verstehen jetzt, dass das Bewegen von l-Werten potenziell gefährlich ist, aber das Bewegen von r-Werten harmlos ist. Wenn C ++ Sprachunterstützung hätte, um lvalue-Argumente von rvalue-Argumenten zu unterscheiden, könnten wir entweder das Verschieben von lvalues ​​vollständig verbieten oder zumindest das Verschieben von lvalues am Aufrufstandort explizit machen , damit wir uns nicht mehr versehentlich bewegen.

Die Antwort von C ++ 11 auf dieses Problem lautet rWertreferenzen . Eine rvalue-Referenz ist eine neue Art von Referenz, die nur an rvalues ​​bindet, und die Syntax lautet X&&. Die gute alte Referenz X&ist jetzt als Wertreferenz bekannt . (Beachten Sie, dass X&&ist nicht ein Verweis auf eine Referenz, es gibt nicht so etwas in C ++.)

Wenn wir constin die Mischung werfen , haben wir bereits vier verschiedene Arten von Referenzen. An welche Arten von Typausdrücken Xkönnen sie binden?

            lvalue   const lvalue   rvalue   const rvalue
---------------------------------------------------------              
X&          yes
const X&    yes      yes            yes      yes
X&&                                 yes
const X&&                           yes      yes

In der Praxis können Sie vergessen const X&&. Es ist nicht sehr nützlich, sich auf das Lesen von Werten zu beschränken.

Eine r-Wert-Referenz X&&ist eine neue Art von Referenz, die nur an r-Werte bindet.

Implizite Konvertierungen

R-Wert-Referenzen durchliefen mehrere Versionen. Seit Version 2.1 X&&bindet eine rvalue-Referenz auch an alle Wertekategorien eines anderen Typs Y, sofern eine implizite Konvertierung von Ynach erfolgt X. In diesem Fall wird ein temporärer Typ Xerstellt, und die r-Wert-Referenz ist an diesen temporären Typ gebunden:

void some_function(std::string&& r);

some_function("hello world");

Im obigen Beispiel "hello world"ist ein Wert vom Typ const char[12]. Da es eine implizite Konvertierung von const char[12]bis const char*nach gibt std::string, wird ein temporäres vom Typ std::stringerstellt und ran dieses temporäre gebunden. Dies ist einer der Fälle, in denen die Unterscheidung zwischen r-Werten (Ausdrücken) und temporären Werten (Objekten) etwas verschwommen ist.

Konstruktoren verschieben

Ein nützliches Beispiel für eine Funktion mit einem X&&Parameter ist der Verschiebungskonstruktor X::X(X&& source) . Der Zweck besteht darin, das Eigentum an der verwalteten Ressource von der Quelle auf das aktuelle Objekt zu übertragen.

In C ++ 11 std::auto_ptr<T>wurde ersetzt, std::unique_ptr<T>wodurch rvalue-Referenzen genutzt werden. Ich werde eine vereinfachte Version von entwickeln und diskutieren unique_ptr. Erstens kapseln wir einen Rohzeiger und überlasten die Betreiber ->und *, so dass unsere Klasse fühlt sich wie ein Zeiger:

template<typename T>
class unique_ptr
{
    T* ptr;

public:

    T* operator->() const
    {
        return ptr;
    }

    T& operator*() const
    {
        return *ptr;
    }

Der Konstruktor übernimmt das Eigentum an dem Objekt und der Destruktor löscht es:

    explicit unique_ptr(T* p = nullptr)
    {
        ptr = p;
    }

    ~unique_ptr()
    {
        delete ptr;
    }

Nun kommt der interessante Teil, der Verschiebungskonstruktor:

    unique_ptr(unique_ptr&& source)   // note the rvalue reference
    {
        ptr = source.ptr;
        source.ptr = nullptr;
    }

Dieser Verschiebungskonstruktor macht genau das, was der auto_ptrKopierkonstruktor getan hat, kann jedoch nur mit rWerten geliefert werden:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);                 // error
unique_ptr<Shape> c(make_triangle());   // okay

Die zweite Zeile kann nicht kompiliert werden, da aes sich um einen l- unique_ptr&& sourceWert handelt. Der Parameter kann jedoch nur an r-Werte gebunden werden. Genau das wollten wir; gefährliche Bewegungen sollten niemals implizit sein. Die dritte Zeile wird gut kompiliert, da make_triangle()es sich um einen Wert handelt. Der Verschiebungskonstruktor überträgt das Eigentum von der temporären auf c. Auch dies ist genau das, was wir wollten.

Der Verschiebungskonstruktor überträgt das Eigentum an einer verwalteten Ressource auf das aktuelle Objekt.

Zuweisungsoperatoren verschieben

Das letzte fehlende Teil ist der Bewegungszuweisungsoperator. Seine Aufgabe ist es, die alte Ressource freizugeben und die neue Ressource aus ihrem Argument zu erhalten:

    unique_ptr& operator=(unique_ptr&& source)   // note the rvalue reference
    {
        if (this != &source)    // beware of self-assignment
        {
            delete ptr;         // release the old resource

            ptr = source.ptr;   // acquire the new resource
            source.ptr = nullptr;
        }
        return *this;
    }
};

Beachten Sie, wie diese Implementierung des Verschiebungszuweisungsoperators die Logik sowohl des Destruktors als auch des Verschiebungskonstruktors dupliziert. Kennen Sie die Copy-and-Swap-Sprache? Es kann auch angewendet werden, um die Semantik als Move-and-Swap-Idiom zu verschieben:

    unique_ptr& operator=(unique_ptr source)   // note the missing reference
    {
        std::swap(ptr, source.ptr);
        return *this;
    }
};

Dies sourceist eine Variable vom Typ unique_ptr, die vom Verschiebungskonstruktor initialisiert wird. Das heißt, das Argument wird in den Parameter verschoben. Das Argument muss weiterhin ein r-Wert sein, da der Verschiebungskonstruktor selbst einen r-Wert-Referenzparameter hat. Wenn der Kontrollfluss die schließende Klammer von erreicht operator=, sourceverlässt er den Gültigkeitsbereich und gibt die alte Ressource automatisch frei.

Der Verschiebungszuweisungsoperator überträgt das Eigentum an einer verwalteten Ressource auf das aktuelle Objekt und gibt die alte Ressource frei. Die Move-and-Swap-Sprache vereinfacht die Implementierung.

Von lWerten weggehen

Manchmal wollen wir uns von lWerten entfernen. Das heißt, manchmal möchten wir, dass der Compiler einen l-Wert so behandelt, als wäre er ein r-Wert, damit er den Verschiebungskonstruktor aufrufen kann, obwohl er möglicherweise unsicher ist. Zu diesem Zweck bietet C ++ 11 eine Standardvorlage für Bibliotheksfunktionen, die std::moveim Header aufgerufen wird <utility>. Dieser Name ist etwas unglücklich, weil er std::moveeinfach einen l-Wert in einen r-Wert umwandelt. es ist nicht alles von selbst bewegen. Es ermöglicht lediglich das Bewegen. Vielleicht hätte es benannt werden sollen std::cast_to_rvalueoder std::enable_move, aber wir bleiben jetzt beim Namen.

So bewegen Sie sich explizit von einem Wert:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);              // still an error
unique_ptr<Shape> c(std::move(a));   // okay

Beachten Sie, dass nach der dritten Zeile akein Dreieck mehr vorhanden ist. Das ist in Ordnung, denn durch explizites Schreiben std::move(a)haben wir unsere Absichten klargestellt: "Lieber Konstruktor, machen Sie, was Sie wollen, aum zu initialisieren c; es interessiert mich nicht amehr. Fühlen Sie sich frei, Ihren Weg mit zu haben a."

std::move(some_lvalue) Wirft einen l-Wert in einen r-Wert und ermöglicht so einen nachfolgenden Zug.

X-Werte

Beachten Sie, dass std::move(a)die Auswertung , obwohl es sich um einen r-Wert handelt, kein temporäres Objekt erstellt. Dieses Rätsel zwang das Komitee, eine dritte Wertekategorie einzuführen. Etwas , das mit einer R - Wert Referenz gebunden werden kann, auch wenn es nicht ein R - Wert im herkömmlichen Sinne ist, ist ein genannt xValue (auslaufend Wert). Die traditionellen Werte wurden in Werte (Reine Werte) umbenannt.

Sowohl prvalues ​​als auch xvalues ​​sind rvalues. X- und l-Werte sind beide gl-Werte (Generalized lvalues). Die Zusammenhänge lassen sich mit einem Diagramm leichter erfassen:

        expressions
          /     \
         /       \
        /         \
    glvalues   rvalues
      /  \       /  \
     /    \     /    \
    /      \   /      \
lvalues   xvalues   prvalues

Beachten Sie, dass nur x-Werte wirklich neu sind. Der Rest ist nur auf das Umbenennen und Gruppieren zurückzuführen.

C ++ 98-Werte werden in C ++ 11 als Werte bezeichnet. Ersetzen Sie geistig alle Vorkommen von "rvalue" in den vorhergehenden Absätzen durch "prvalue".

Funktionen verlassen

Bisher haben wir eine Bewegung in lokale Variablen und in Funktionsparameter gesehen. Eine Bewegung ist aber auch in die entgegengesetzte Richtung möglich. Wenn eine Funktion nach Wert zurückgegeben wird, wird ein Objekt am Aufrufstandort (wahrscheinlich eine lokale Variable oder ein temporäres Objekt, kann aber eine beliebige Art von Objekt sein) mit dem Ausdruck nach der returnAnweisung als Argument für den Verschiebungskonstruktor initialisiert :

unique_ptr<Shape> make_triangle()
{
    return unique_ptr<Shape>(new Triangle);
}          \-----------------------------/
                  |
                  | temporary is moved into c
                  |
                  v
unique_ptr<Shape> c(make_triangle());

Es ist vielleicht überraschend, dass automatische Objekte (lokale Variablen, die nicht als deklariert sind static) auch implizit aus Funktionen verschoben werden können:

unique_ptr<Shape> make_square()
{
    unique_ptr<Shape> result(new Square);
    return result;   // note the missing std::move
}

Wie kommt es, dass der Verschiebungskonstruktor den l-Wert resultals Argument akzeptiert ? Der Umfang von resultist kurz vor dem Ende und wird beim Abwickeln des Stapels zerstört. Niemand konnte sich danach beschweren, dass resultsich das irgendwie geändert hatte; Wenn der Kontrollfluss wieder beim Anrufer ist, resultexistiert er nicht mehr! Aus diesem Grund verfügt C ++ 11 über eine spezielle Regel, die es ermöglicht, automatische Objekte von Funktionen zurückzugeben, ohne schreiben zu müssen std::move. Tatsächlich sollten Sie niemals verwenden std::move, um automatische Objekte aus Funktionen zu verschieben, da dies die "Named Return Value Optimization" (NRVO) verhindert.

Verwenden Sie std::movediese Option niemals , um automatische Objekte aus Funktionen zu entfernen.

Beachten Sie, dass in beiden Werksfunktionen der Rückgabetyp ein Wert und keine r-Wert-Referenz ist. R-Wert-Referenzen sind immer noch Referenzen, und wie immer sollten Sie niemals eine Referenz auf ein automatisches Objekt zurückgeben. Der Aufrufer würde eine baumelnde Referenz erhalten, wenn Sie den Compiler dazu verleiten würden, Ihren Code wie folgt zu akzeptieren:

unique_ptr<Shape>&& flawed_attempt()   // DO NOT DO THIS!
{
    unique_ptr<Shape> very_bad_idea(new Square);
    return std::move(very_bad_idea);   // WRONG!
}

Geben Sie niemals automatische Objekte als Wertreferenz zurück. Das Verschieben wird ausschließlich vom Verschiebungskonstruktor ausgeführt, nicht von std::moveund nicht nur durch Binden eines r-Werts an eine r-Wert-Referenz.

Umzug in Mitglieder

Früher oder später werden Sie Code wie folgt schreiben:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(parameter)   // error
    {}
};

Grundsätzlich wird sich der Compiler beschweren, dass dies parameterein Wert ist. Wenn Sie sich den Typ ansehen, sehen Sie eine r-Wert-Referenz, aber eine r-Wert-Referenz bedeutet einfach "eine Referenz, die an einen r-Wert gebunden ist". es bedeutet nicht , dass die Referenz selbst ein Wert ist! In der Tat parameterist nur eine gewöhnliche Variable mit einem Namen. Sie können parameterim Body des Konstruktors so oft verwenden, wie Sie möchten, und es wird immer dasselbe Objekt bezeichnet. Es wäre gefährlich, sich implizit davon zu entfernen, daher verbietet es die Sprache.

Eine benannte rvalue-Referenz ist wie jede andere Variable ein lvalue.

Die Lösung besteht darin, den Umzug manuell zu aktivieren:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(std::move(parameter))   // note the std::move
    {}
};

Man könnte argumentieren, dass parameterdas nach der Initialisierung von nicht mehr verwendet wird member. Warum gibt es keine spezielle Regel zum stillen Einfügen std::movewie bei Rückgabewerten? Wahrscheinlich, weil es die Compiler-Implementierer zu sehr belasten würde. Was wäre zum Beispiel, wenn sich der Konstruktorkörper in einer anderen Übersetzungseinheit befindet? Im Gegensatz dazu muss die Rückgabewertregel lediglich die Symboltabellen überprüfen, um festzustellen, ob der Bezeichner nach dem returnSchlüsselwort ein automatisches Objekt bezeichnet oder nicht .

Sie können den parameterby-Wert auch übergeben. Für Nur-Bewegung-Typen wie unique_ptrscheint es noch keine etablierte Sprache zu geben. Persönlich übergebe ich lieber den Wert, da dies weniger Unordnung in der Benutzeroberfläche verursacht.

Spezielle Mitgliedsfunktionen

C ++ 98 deklariert implizit drei spezielle Elementfunktionen bei Bedarf, dh wenn sie irgendwo benötigt werden: den Kopierkonstruktor, den Kopierzuweisungsoperator und den Destruktor.

X::X(const X&);              // copy constructor
X& X::operator=(const X&);   // copy assignment operator
X::~X();                     // destructor

R-Wert-Referenzen durchliefen mehrere Versionen. Seit Version 3.0 deklariert C ++ 11 bei Bedarf zwei zusätzliche spezielle Elementfunktionen: den Verschiebungskonstruktor und den Verschiebungszuweisungsoperator. Beachten Sie, dass weder VC10 noch VC11 noch Version 3.0 entsprechen, sodass Sie sie selbst implementieren müssen.

X::X(X&&);                   // move constructor
X& X::operator=(X&&);        // move assignment operator

Diese beiden neuen speziellen Elementfunktionen werden nur implizit deklariert, wenn keine der speziellen Elementfunktionen manuell deklariert wird. Wenn Sie Ihren eigenen Verschiebungskonstruktor oder Verschiebungszuweisungsoperator deklarieren, werden weder der Kopierkonstruktor noch der Kopierzuweisungsoperator implizit deklariert.

Was bedeuten diese Regeln in der Praxis?

Wenn Sie eine Klasse ohne nicht verwaltete Ressourcen schreiben, müssen Sie keine der fünf speziellen Elementfunktionen selbst deklarieren, und Sie erhalten kostenlos die richtige Kopiersemantik und Verschiebungssemantik. Andernfalls müssen Sie die speziellen Elementfunktionen selbst implementieren. Wenn Ihre Klasse nicht von der Verschiebungssemantik profitiert, müssen die speziellen Verschiebungsoperationen natürlich nicht implementiert werden.

Beachten Sie, dass der Kopierzuweisungsoperator und der Verschiebungszuweisungsoperator zu einem einzigen einheitlichen Zuweisungsoperator zusammengeführt werden können, wobei das Argument nach Wert sortiert wird:

X& X::operator=(X source)    // unified assignment operator
{
    swap(source);            // see my first answer for an explanation
    return *this;
}

Auf diese Weise sinkt die Anzahl der zu implementierenden speziellen Elementfunktionen von fünf auf vier. Hier gibt es einen Kompromiss zwischen Ausnahmesicherheit und Effizienz, aber ich bin kein Experte in diesem Bereich.

Weiterleitungsreferenzen ( früher als Universalreferenzen bekannt )

Betrachten Sie die folgende Funktionsvorlage:

template<typename T>
void foo(T&&);

Sie können erwarten T&&, nur an r-Werte zu binden, da es auf den ersten Blick wie eine r-Wert-Referenz aussieht. Wie sich jedoch herausstellt, T&&bindet auch an lWerte:

foo(make_triangle());   // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a);                 // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&

Wenn das Argument ein R - Wert vom Typ ist X, Twird abgeleitet zu sein X, daher T&&bedeutet X&&. Das würde jeder erwarten. Wenn es sich bei dem Argument jedoch um einen Wert vom Typ handelt X, wird aufgrund einer Sonderregel davon ausgegangen, dass dies der TFall ist X&, und T&&würde daher so etwas wie bedeuten X& &&. Aber da C ++ noch keine Ahnung von Verweisen auf Referenzen hat, die Art X& &&ist , kollabiert in X&. Dies mag zunächst verwirrend und nutzlos klingen, aber das Zusammenfallen von Referenzen ist für eine perfekte Weiterleitung unerlässlich (auf die hier nicht näher eingegangen wird).

T && ist keine Wertreferenz, sondern eine Weiterleitungsreferenz. Es bindet auch an lWerte, in diesem Fall Tund T&&sind beide lWertreferenzen.

Wenn Sie eine Funktionsvorlage auf rWerte beschränken möchten, können Sie SFINAE mit Typmerkmalen kombinieren :

#include <type_traits>

template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);

Umsetzung des Umzugs

Nachdem Sie nun das Reduzieren von Referenzen verstanden haben, wird Folgendes std::moveimplementiert:

template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

Wie Sie sehen können, moveakzeptiert dank der Weiterleitungsreferenz jede Art von Parameter T&&und gibt eine rWert-Referenz zurück. Der std::remove_reference<T>::typeMetafunktionsaufruf ist notwendig, da andernfalls für lWerte vom Typ Xder Rückgabetyp wäre X& &&, der zusammenbrechen würde X&. Da tes sich immer um einen l-Wert handelt (denken Sie daran, dass eine benannte r-Wert-Referenz ein l-Wert ist), wir aber tan eine r-Wert-Referenz binden möchten, müssen wir explizit tin den richtigen Rückgabetyp umwandeln . Der Aufruf einer Funktion, die eine rvalue-Referenz zurückgibt, ist selbst ein xvalue. Jetzt weißt du woher xvalues ​​kommen;)

Der Aufruf einer Funktion, die eine r-Wert-Referenz zurückgibt, z. B. std::moveein x-Wert.

Beachten Sie, dass die Rückgabe per rvalue-Referenz in diesem Beispiel in Ordnung ist, da tdies kein automatisches Objekt bezeichnet, sondern ein Objekt, das vom Aufrufer übergeben wurde.

Fredoverflow
quelle
24
Es gibt einen dritten Grund, warum Bewegungssemantik wichtig ist: Ausnahmesicherheit. Wenn ein Kopiervorgang möglicherweise ausgelöst wird (weil Ressourcen zugewiesen werden müssen und die Zuweisung möglicherweise fehlschlägt), kann ein Verschiebevorgang häufig nicht ausgelöst werden (weil er den Besitz vorhandener Ressourcen übertragen kann, anstatt neue Ressourcen zuzuweisen). Operationen, die nicht fehlschlagen können, sind immer hilfreich, und sie können beim Schreiben von Code mit Ausnahmegarantien von entscheidender Bedeutung sein.
Brangdon
8
Ich war bis zu 'Universal References' bei Ihnen, aber dann ist es alles zu abstrakt, um zu folgen. Referenz kollabiert? Perfekte Weiterleitung? Wollen Sie damit sagen, dass eine r-Wert-Referenz zu einer universellen Referenz wird, wenn der Typ als Vorlage verwendet wird? Ich wünschte, es gäbe eine Möglichkeit, dies zu erklären, damit ich weiß, ob ich es verstehen muss oder nicht! :)
Kylotan
8
Bitte schreiben Sie jetzt ein Buch ... Diese Antwort hat mir Grund zu der Annahme gegeben, dass Tausende von Menschen es verstehen werden, wenn Sie andere Ecken von C ++ auf eine so klare Weise behandelt haben.
Halivingston
12
@halivingston Vielen Dank für Ihr freundliches Feedback, ich schätze es sehr. Das Problem beim Schreiben eines Buches ist: Es ist viel mehr Arbeit, als Sie sich vorstellen können. Wenn Sie tief in C ++ 11 und darüber hinaus eintauchen möchten, empfehlen wir Ihnen, "Effective Modern C ++" von Scott Meyers zu kaufen.
Fredoverflow
77

Die Verschiebungssemantik basiert auf rWertreferenzen .
Ein r-Wert ist ein temporäres Objekt, das am Ende des Ausdrucks zerstört wird. In aktuellem C ++ binden r-Werte nur an constReferenzen. C ++ 1x erlaubt constbuchstabierte Nicht- Wert-Referenzen T&&, die Referenzen auf ein Wert-Objekt sind.
Da ein r-Wert am Ende eines Ausdrucks sterben wird, können Sie seine Daten stehlen . Anstatt das Kopieren in ein anderes Objekt, Sie bewegen ihre Daten hinein.

class X {
public: 
  X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
    : data_()
  {
     // since 'x' is an rvalue object, we can steal its data
     this->swap(std::move(rhs));
     // this will leave rhs with the empty data
  }
  void swap(X&& rhs);
  // ... 
};

// ...

X f();

X x = f(); // f() returns result as rvalue, so this calls move-ctor

In dem obigen Code, mit alten Compiler des Ergebnisses f()wird kopiert in xVerwendung X‚s Copykonstruktor. Wenn Ihr Compiler die Verschiebungssemantik unterstützt und Xüber einen Verschiebungskonstruktor verfügt, wird dieser stattdessen aufgerufen. Da sein rhsArgument ein Wert ist , wissen wir, dass es nicht mehr benötigt wird und wir können seinen Wert stehlen.
Der Wert wird also von dem unbenannten temporären Wert verschoben, der von f()bis zurückgegeben wird x(während die Daten von x, die auf einen leeren Wert initialisiert wurden, Xin den temporären Wert verschoben werden, der nach der Zuweisung zerstört wird).

sbi
quelle
1
Beachten Sie, dass es sein sollte, this->swap(std::move(rhs));weil benannte rWertreferenzen lWerte sind
wmamrak
Dies ist rhsein bisschen falsch, laut @ Tacyts Kommentar: ist ein Wert im Kontext von X::X(X&& rhs). Sie müssen anrufen std::move(rhs), um einen Wert zu erhalten, aber dies macht die Antwort irgendwie umstritten.
Asherah
Was bedeutet Bewegungssemantik für Typen ohne Zeiger? Semantik verschieben funktioniert wie eine Kopie?
Gusev Slava
@ Gusev: Ich habe keine Ahnung, was Sie fragen.
sbi
60

Angenommen, Sie haben eine Funktion, die ein wesentliches Objekt zurückgibt:

Matrix multiply(const Matrix &a, const Matrix &b);

Wenn Sie Code wie folgt schreiben:

Matrix r = multiply(a, b);

Dann erstellt ein gewöhnlicher C ++ - Compiler ein temporäres Objekt für das Ergebnis von multiply(), ruft den Kopierkonstruktor zum Initialisieren auf rund zerstört dann den temporären Rückgabewert. Mit der Verschiebungssemantik in C ++ 0x kann der "Verschiebungskonstruktor" aufgerufen werden, um rdurch Kopieren seines Inhalts zu initialisieren , und der temporäre Wert wird dann verworfen, ohne ihn zerstören zu müssen.

Dies ist besonders wichtig, wenn (wie im Matrixobigen Beispiel) das zu kopierende Objekt zusätzlichen Speicher auf dem Heap zuweist, um seine interne Darstellung zu speichern. Ein Kopierkonstruktor müsste entweder eine vollständige Kopie der internen Darstellung erstellen oder intern die Referenzzählung und die Semantik des Kopierens beim Schreiben verwenden. Ein Verschiebungskonstruktor würde den Heapspeicher in Ruhe lassen und einfach den Zeiger in das MatrixObjekt kopieren .

Greg Hewgill
quelle
2
Wie unterscheiden sich Verschiebungskonstruktoren und Kopierkonstruktoren?
Dicroce
1
@dicroce: Sie unterscheiden sich durch die Syntax, eine sieht aus wie Matrix (const Matrix & src) (Kopierkonstruktor) und die andere sieht aus wie Matrix (Matrix && src) (Verschiebungskonstruktor). Überprüfen Sie meine Hauptantwort auf ein besseres Beispiel.
snk_kid
3
@dicroce: Man macht ein leeres Objekt und man macht eine Kopie. Wenn die im Objekt gespeicherten Daten groß sind, kann eine Kopie teuer sein. Zum Beispiel std :: vector.
Billy ONeal
1
@ kunj2aan: Es hängt von deinem Compiler ab, vermute ich. Der Compiler kann ein temporäres Objekt innerhalb der Funktion erstellen und es dann in den Rückgabewert des Aufrufers verschieben. Möglicherweise kann das Objekt auch direkt im Rückgabewert erstellt werden, ohne dass ein Verschiebungskonstruktor verwendet werden muss.
Greg Hewgill
2
@Jichao: Das ist eine Optimierung namens RVO, siehe diese Frage für weitere Informationen über den Unterschied: stackoverflow.com/questions/5031778/…
Greg Hewgill
30

Wenn Sie wirklich an einer guten, ausführlichen Erklärung der Verschiebungssemantik interessiert sind, empfehle ich dringend, das Originalpapier zu lesen: "Ein Vorschlag, der C ++ - Sprache Unterstützung für Verschiebungssemantik hinzuzufügen."

Es ist sehr leicht zugänglich und leicht zu lesen und ein hervorragendes Argument für die Vorteile, die sie bieten. Es gibt andere neuere und aktuellere Artikel über die Bewegungssemantik auf der WG21-Website , aber dieser ist wahrscheinlich der einfachste, da er sich den Dingen aus einer Top-Level-Sicht nähert und nicht sehr auf die Details der Sprache eingeht.

James McNellis
quelle
27

Bei der Verschiebungssemantik geht es darum , Ressourcen zu übertragen, anstatt sie zu kopieren, wenn niemand mehr den Quellwert benötigt.

In C ++ 03 werden Objekte häufig kopiert, nur um zerstört oder zugewiesen zu werden, bevor ein Code den Wert erneut verwendet. Wenn Sie beispielsweise einen Wert von einer Funktion zurückgeben - es sei denn, RVO wird aktiviert -, wird der zurückgegebene Wert in den Stapelrahmen des Aufrufers kopiert, verlässt den Gültigkeitsbereich und wird zerstört. Dies ist nur eines von vielen Beispielen: Siehe Pass-by-Value, wenn das Quellobjekt temporär ist, Algorithmen wie sortdiese ordnen Elemente einfach neu an, ordnen sie neu zu, vectorwenn sie capacity()überschritten werden usw.

Wenn solche Kopier- / Zerstörungspaare teuer sind, liegt dies normalerweise daran, dass das Objekt eine schwere Ressource besitzt. vector<string>Kann beispielsweise einen dynamisch zugewiesenen Speicherblock besitzen, der ein Array von stringObjekten enthält, von denen jedes seinen eigenen dynamischen Speicher hat. Das Kopieren eines solchen Objekts ist kostspielig: Sie müssen jedem dynamisch zugewiesenen Block in der Quelle neuen Speicher zuweisen und alle Werte kopieren. Dann müssen Sie den gesamten Speicher, den Sie gerade kopiert haben, freigeben. Das Verschieben eines großen vector<string>Zeigers bedeutet jedoch, dass nur einige Zeiger (die sich auf den dynamischen Speicherblock beziehen) auf das Ziel kopiert und in der Quelle auf Null gesetzt werden.

Dave Abrahams
quelle
23

In einfachen (praktischen) Begriffen:

Das Kopieren eines Objekts bedeutet das Kopieren seiner "statischen" Elemente und das Aufrufen des newOperators für seine dynamischen Objekte. Recht?

class A
{
   int i, *p;

public:
   A(const A& a) : i(a.i), p(new int(*a.p)) {}
   ~A() { delete p; }
};

Das Verschieben eines Objekts (ich wiederhole es aus praktischer Sicht) bedeutet jedoch, nur die Zeiger dynamischer Objekte zu kopieren und keine neuen zu erstellen.

Aber ist das nicht gefährlich? Natürlich können Sie ein dynamisches Objekt zweimal zerstören (Segmentierungsfehler). Um dies zu vermeiden, sollten Sie die Quellzeiger "ungültig machen", um zu vermeiden, dass sie zweimal zerstört werden:

class A
{
   int i, *p;

public:
   // Movement of an object inside a copy constructor.
   A(const A& a) : i(a.i), p(a.p)
   {
     a.p = nullptr; // pointer invalidated.
   }

   ~A() { delete p; }
   // Deleting NULL, 0 or nullptr (address 0x0) is safe. 
};

Ok, aber wenn ich ein Objekt verschiebe, wird das Quellobjekt unbrauchbar, nein? Natürlich, aber in bestimmten Situationen ist das sehr nützlich. Das offensichtlichste ist, wenn ich eine Funktion mit einem anonymen Objekt aufrufe (temporales, rvalue Objekt, ..., Sie können es mit verschiedenen Namen aufrufen):

void heavyFunction(HeavyType());

In dieser Situation wird ein anonymes Objekt erstellt, als nächstes in den Funktionsparameter kopiert und anschließend gelöscht. Hier ist es also besser, das Objekt zu verschieben, da Sie das anonyme Objekt nicht benötigen und Zeit und Speicher sparen können.

Dies führt zum Konzept einer "rvalue" -Referenz. Sie sind in C ++ 11 nur vorhanden, um festzustellen, ob das empfangene Objekt anonym ist oder nicht. Ich denke, Sie wissen bereits, dass ein "l-Wert" eine zuweisbare Entität ist (der linke Teil des =Operators), daher benötigen Sie einen benannten Verweis auf ein Objekt, um als l-Wert fungieren zu können. Ein rWert ist genau das Gegenteil, ein Objekt ohne benannte Referenzen. Aus diesem Grund sind anonymes Objekt und r-Wert Synonyme. Damit:

class A
{
   int i, *p;

public:
   // Copy
   A(const A& a) : i(a.i), p(new int(*a.p)) {}

   // Movement (&& means "rvalue reference to")
   A(A&& a) : i(a.i), p(a.p)
   {
      a.p = nullptr;
   }

   ~A() { delete p; }
};

In diesem Fall Aerstellt der Compiler , wenn ein Objekt vom Typ "kopiert" werden soll, eine l-Wert-Referenz oder eine r-Wert-Referenz, je nachdem, ob das übergebene Objekt benannt ist oder nicht. Wenn nicht, wird Ihr Verschiebungskonstruktor aufgerufen und Sie wissen, dass das Objekt zeitlich begrenzt ist. Sie können seine dynamischen Objekte verschieben, anstatt sie zu kopieren, wodurch Platz und Speicherplatz gespart werden.

Es ist wichtig zu beachten, dass "statische" Objekte immer kopiert werden. Es gibt keine Möglichkeit, ein statisches Objekt (Objekt im Stapel und nicht auf dem Heap) zu "verschieben". Daher ist die Unterscheidung "Verschieben" / "Kopieren", wenn ein Objekt keine dynamischen Elemente (direkt oder indirekt) hat, irrelevant.

Wenn Ihr Objekt komplex ist und der Destruktor andere sekundäre Effekte hat, z. B. das Aufrufen der Funktion einer Bibliothek, das Aufrufen anderer globaler Funktionen oder was auch immer, ist es möglicherweise besser, eine Bewegung mit einem Flag zu signalisieren:

class Heavy
{
   bool b_moved;
   // staff

public:
   A(const A& a) { /* definition */ }
   A(A&& a) : // initialization list
   {
      a.b_moved = true;
   }

   ~A() { if (!b_moved) /* destruct object */ }
};

Ihr Code ist also kürzer (Sie müssen nicht nullptrfür jedes dynamische Mitglied eine Zuweisung vornehmen) und allgemeiner.

Andere typische Frage: Was ist der Unterschied zwischen A&&und const A&&? Natürlich können Sie im ersten Fall das Objekt ändern und im zweiten Fall nicht, aber praktische Bedeutung? Im zweiten Fall können Sie es nicht ändern, sodass Sie keine Möglichkeit haben, das Objekt ungültig zu machen (außer mit einem veränderlichen Flag oder ähnlichem), und es gibt keinen praktischen Unterschied zu einem Kopierkonstruktor.

Und was ist perfekte Weiterleitung ? Es ist wichtig zu wissen, dass eine "rWertreferenz" eine Referenz auf ein benanntes Objekt im "Bereich des Aufrufers" ist. Im tatsächlichen Bereich ist eine r-Wert-Referenz jedoch ein Name für ein Objekt, sodass sie als benanntes Objekt fungiert. Wenn Sie einen r-Wert-Verweis an eine andere Funktion übergeben, übergeben Sie ein benanntes Objekt, sodass das Objekt nicht wie ein temporäres Objekt empfangen wird.

void some_function(A&& a)
{
   other_function(a);
}

Das Objekt awürde in den aktuellen Parameter von kopiert other_function. Wenn Sie möchten, dass das Objekt aweiterhin als temporäres Objekt behandelt wird, sollten Sie die folgende std::moveFunktion verwenden:

other_function(std::move(a));

std::moveWird mit dieser Zeile ain einen r-Wert umgewandelt und other_functionerhält das Objekt als unbenanntes Objekt. Wenn other_functionkeine spezifische Überladung für die Arbeit mit unbenannten Objekten vorliegt, ist diese Unterscheidung natürlich nicht wichtig.

Ist das perfekte Weiterleitung? Nicht, aber wir stehen uns sehr nahe. Perfekte Weiterleitung ist nur nützlich, um mit Vorlagen zu arbeiten, mit dem Ziel zu sagen: Wenn ich ein Objekt an eine andere Funktion übergeben muss, muss das Objekt als benanntes Objekt übergeben werden, wenn ich ein benanntes Objekt erhalte, und wenn nicht, Ich möchte es wie ein unbenanntes Objekt übergeben:

template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}

Dies ist die Signatur einer prototypischen Funktion, die eine perfekte Weiterleitung verwendet und in C ++ 11 mithilfe von implementiert wurde std::forward. Diese Funktion nutzt einige Regeln für die Instanziierung von Vorlagen:

 `A& && == A&`
 `A&& && == A&&`

Wenn Talso ein Wert auf A( T = A &) verweist , aauch ( A & && => A &). Wenn Tist ein rWert Verweis auf A, aauch (A && && => A &&). In beiden Fällen ahandelt es sich um ein benanntes Objekt im tatsächlichen Bereich, Tenthält jedoch die Informationen seines "Referenztyps" aus Sicht des Aufruferbereichs. Diese Information ( T) wird als Vorlagenparameter an übergeben forwardund 'a' wird je nach Typ von verschoben oder nicht T.

Peregring-lk
quelle
20

Es ist wie eine Kopiersemantik, aber anstatt alle Daten zu duplizieren, müssen Sie die Daten von dem Objekt stehlen, von dem "verschoben" wird.

Terry Mahaffey
quelle
13

Sie wissen, was eine Kopiersemantik bedeutet, oder? Dies bedeutet, dass Sie Typen haben, die kopierbar sind. Für benutzerdefinierte Typen definieren Sie diese entweder explizit, indem Sie einen Kopierkonstruktor und einen Zuweisungsoperator schreiben, oder der Compiler generiert sie implizit. Dadurch wird eine Kopie erstellt.

Die Verschiebungssemantik ist im Grunde ein benutzerdefinierter Typ mit Konstruktor, der eine r-Wert-Referenz (neuer Referenztyp mit && (ja zwei kaufmännische Und-Zeichen)) verwendet, die nicht konstant ist. Dies wird als Verschiebungskonstruktor bezeichnet. Gleiches gilt für den Zuweisungsoperator. Was macht ein Verschiebungskonstruktor? Anstatt Speicher aus seinem Quellargument zu kopieren, verschiebt er Speicher von der Quelle zum Ziel.

Wann möchten Sie das tun? Nun, std :: vector ist ein Beispiel. Angenommen, Sie haben einen temporären std :: vector erstellt und geben ihn von einer Funktion zurück, z. B.:

std::vector<foo> get_foos();

Wenn die Funktion zurückkehrt, wird der Kopierkonstruktor Overhead haben, wenn (und in C ++ 0x) std :: vector einen Verschiebungskonstruktor hat, anstatt ihn zu kopieren, kann er einfach seine Zeiger setzen und 'Verschieben' dynamisch zuweisen Speicher für die neue Instanz. Es ist eine Art Semantik der Eigentumsübertragung mit std :: auto_ptr.

snk_kid
quelle
1
Ich denke nicht, dass dies ein großartiges Beispiel ist, da in diesen Beispielen für Rückgabewerte von Funktionen die Rückgabewertoptimierung den Kopiervorgang wahrscheinlich bereits eliminiert.
Zan Lynx
7

Um die Notwendigkeit einer Verschiebungssemantik zu veranschaulichen , betrachten wir dieses Beispiel ohne Verschiebungssemantik:

Hier ist eine Funktion, die ein Objekt vom Typ nimmt Tund ein Objekt vom gleichen Typ zurückgibt T:

T f(T o) { return o; }
  //^^^ new object constructed

Die oben genannte Funktion verwendet ruft nach Wert , was bedeutet , dass , wenn diese Funktion ein Objekt aufgerufen wird , muss konstruiert von der Funktion verwendet werden.
Da die Funktion auch nach Wert zurückgibt , wird ein weiteres neues Objekt für den Rückgabewert erstellt:

T b = f(a);
  //^ new object constructed

Es wurden zwei neue Objekte erstellt, von denen eines ein temporäres Objekt ist, das nur für die Dauer der Funktion verwendet wird.

Wenn das neue Objekt aus dem Rückgabewert erstellt wird, wird der Kopierkonstruktor aufgerufen, um den Inhalt des temporären Objekts in das neue Objekt zu kopieren . B. Nach Abschluss der Funktion verlässt das in der Funktion verwendete temporäre Objekt den Gültigkeitsbereich und wird zerstört.


Betrachten wir nun, was ein Kopierkonstruktor tut.

Es muss zuerst das Objekt initialisieren und dann alle relevanten Daten vom alten Objekt auf das neue kopieren.
Abhängig von der Klasse ist es möglicherweise ein Container mit sehr vielen Daten, der viel Zeit und Speicherplatz beanspruchen kann

// Copy constructor
T::T(T &old) {
    copy_data(m_a, old.m_a);
    copy_data(m_b, old.m_b);
    copy_data(m_c, old.m_c);
}

Mit move Semantik ist es nun möglich , die meisten dieser Arbeit weniger unangenehm , indem Sie einfach zu machen Bewegen der Daten anstatt zu kopieren.

// Move constructor
T::T(T &&old) noexcept {
    m_a = std::move(old.m_a);
    m_b = std::move(old.m_b);
    m_c = std::move(old.m_c);
}

Beim Verschieben der Daten werden die Daten erneut dem neuen Objekt zugeordnet. Und es findet überhaupt keine Kopie statt .

Dies wird mit einer rvalueReferenz erreicht.
Eine rvalueReferenz funktioniert ziemlich ähnlich wie eine lvalueReferenz mit einem wichtigen Unterschied:
Eine r-Wert-Referenz kann verschoben werden und eine l- Wert-Referenz nicht.

Von cppreference.com :

Um eine starke Ausnahmegarantie zu ermöglichen, sollten benutzerdefinierte Verschiebungskonstruktoren keine Ausnahmen auslösen. Tatsächlich verlassen sich Standardcontainer normalerweise auf std :: move_if_noexcept, um zwischen Verschieben und Kopieren zu wählen, wenn Containerelemente verschoben werden müssen. Wenn sowohl Kopier- als auch Verschiebungskonstruktoren bereitgestellt werden, wählt die Überladungsauflösung den Verschiebungskonstruktor aus, wenn das Argument ein r-Wert ist (entweder ein Wert wie ein namenloser temporärer Wert oder ein x-Wert wie das Ergebnis von std :: move), und wählt den Kopierkonstruktor aus, wenn Das Argument ist ein l-Wert (benanntes Objekt oder eine Funktion / ein Operator, der / der eine l-Wert-Referenz zurückgibt). Wenn nur der Kopierkonstruktor bereitgestellt wird, wählen ihn alle Argumentkategorien aus (solange ein Verweis auf const erforderlich ist, da r-Werte an const-Verweise gebunden werden können), wodurch das Kopieren des Fallbacks für das Verschieben beim Verschieben nicht verfügbar ist. In vielen Situationen werden Bewegungskonstruktoren optimiert, selbst wenn sie beobachtbare Nebenwirkungen hervorrufen würden, siehe Kopierentfernung. Ein Konstruktor wird als 'Verschiebungskonstruktor' bezeichnet, wenn er eine r-Wert-Referenz als Parameter verwendet. Es ist nicht verpflichtet, etwas zu verschieben, die Klasse muss keine zu verschiebende Ressource haben, und ein 'Verschiebungskonstruktor' kann eine Ressource möglicherweise nicht wie im zulässigen (aber möglicherweise nicht sinnvollen) Fall verschieben, in dem der Parameter a ist const rvalue Referenz (const T &&).

Andreas DM
quelle
7

Ich schreibe dies, um sicherzustellen, dass ich es richtig verstehe.

Die Verschiebungssemantik wurde erstellt, um das unnötige Kopieren großer Objekte zu vermeiden. Bjarne Stroustrup verwendet in seinem Buch "The C ++ Programming Language" zwei Beispiele, bei denen standardmäßig unnötiges Kopieren auftritt: eines, das Austauschen von zwei großen Objekten und zwei, das Zurückgeben eines großen Objekts von einer Methode.

Das Austauschen von zwei großen Objekten umfasst normalerweise das Kopieren des ersten Objekts in ein temporäres Objekt, das Kopieren des zweiten Objekts in das erste Objekt und das Kopieren des temporären Objekts in das zweite Objekt. Für einen eingebauten Typ ist dies sehr schnell, aber für große Objekte können diese drei Kopien viel Zeit in Anspruch nehmen. Eine "Verschiebungszuweisung" ermöglicht es dem Programmierer, das Standardkopierverhalten zu überschreiben und stattdessen Verweise auf die Objekte auszutauschen, was bedeutet, dass überhaupt nicht kopiert wird und der Auslagerungsvorgang viel schneller ist. Die Verschiebungszuweisung kann durch Aufrufen der Methode std :: move () aufgerufen werden.

Das Zurückgeben eines Objekts von einer Methode umfasst standardmäßig das Erstellen einer Kopie des lokalen Objekts und der zugehörigen Daten an einem Ort, auf den der Aufrufer zugreifen kann (da das lokale Objekt für den Aufrufer nicht zugänglich ist und nach Abschluss der Methode verschwindet). Wenn ein integrierter Typ zurückgegeben wird, ist dieser Vorgang sehr schnell. Wenn jedoch ein großes Objekt zurückgegeben wird, kann dies lange dauern. Der Verschiebungskonstruktor ermöglicht es dem Programmierer, dieses Standardverhalten zu überschreiben und stattdessen die dem lokalen Objekt zugeordneten Heap-Daten "wiederzuverwenden", indem das an den Aufrufer zurückgegebene Objekt auf die dem lokalen Objekt zugeordneten Heap-Daten verweist. Somit ist kein Kopieren erforderlich.

In Sprachen, in denen keine lokalen Objekte erstellt werden können (d. H. Objekte auf dem Stapel), treten diese Arten von Problemen nicht auf, da alle Objekte auf dem Heap zugeordnet sind und immer als Referenz zugegriffen wird.

Chris B.
quelle
"Eine" Verschiebungszuweisung "ermöglicht es dem Programmierer, das Standardkopierverhalten zu überschreiben und stattdessen Verweise auf die Objekte auszutauschen, was bedeutet, dass überhaupt nicht kopiert wird und der Auslagerungsvorgang viel schneller ist." - Diese Behauptungen sind mehrdeutig und irreführend. So tauschen zwei Objekte xund ykönnen Sie nicht nur „swap Verweise auf die Objekte“ ; Es kann sein, dass die Objekte Zeiger enthalten, die auf andere Daten verweisen, und diese Zeiger können ausgetauscht werden, aber Verschiebungsoperatoren müssen nichts austauschen. Sie können die Daten aus dem verschobenen Objekt löschen, anstatt die darin enthaltenen Zieldaten beizubehalten.
Tony Delroy
Sie könnten swap()ohne Bewegungssemantik schreiben . "Die Verschiebungszuweisung kann durch Aufrufen der Methode std :: move () aufgerufen werden." - Es ist manchmal notwendig zu verwenden std::move()- obwohl das eigentlich nichts verschiebt - lässt den Compiler nur wissen, dass das Argument beweglich ist, manchmal std::forward<>()(mit Weiterleitungsreferenzen), und manchmal weiß der Compiler, dass ein Wert verschoben werden kann.
Tony Delroy
-2

Hier ist eine Antwort aus dem Buch "The C ++ Programming Language" von Bjarne Stroustrup. Wenn Sie das Video nicht sehen möchten, sehen Sie den folgenden Text:

Betrachten Sie diesen Ausschnitt. Bei der Rückkehr von einem Operator + wird das Ergebnis aus der lokalen Variablen resan einen Ort kopiert, an dem der Aufrufer darauf zugreifen kann.

Vector operator+(const Vector& a, const Vector& b)
{
    if (a.size()!=b.size())
        throw Vector_siz e_mismatch{};
    Vector res(a.size());
        for (int i=0; i!=a.size(); ++i)
            res[i]=a[i]+b[i];
    return res;
}

Wir wollten eigentlich keine Kopie; Wir wollten nur das Ergebnis aus einer Funktion herausholen. Wir müssen also einen Vektor verschieben, anstatt ihn zu kopieren. Wir können den Verschiebungskonstruktor wie folgt definieren:

class Vector {
    // ...
    Vector(const Vector& a); // copy constructor
    Vector& operator=(const Vector& a); // copy assignment
    Vector(Vector&& a); // move constructor
    Vector& operator=(Vector&& a); // move assignment
};

Vector::Vector(Vector&& a)
    :elem{a.elem}, // "grab the elements" from a
    sz{a.sz}
{
    a.elem = nullptr; // now a has no elements
    a.sz = 0;
}

Das && bedeutet "rWertreferenz" und ist eine Referenz, an die wir einen rWert binden können. "rvalue" 'soll "lvalue" ergänzen, was ungefähr "etwas bedeutet, das auf der linken Seite einer Aufgabe erscheinen kann". Ein r-Wert bedeutet also ungefähr "einen Wert, dem Sie nicht zuweisen können", z. B. eine von einem Funktionsaufruf zurückgegebene Ganzzahl und die reslokale Variable in Operator + () für Vektoren.

Jetzt wird die Anweisung return res;nicht kopiert!

Rob Pei
quelle