Frage Was sind Bewegungssemantiken?


Ich habe gerade das Software Engineering Radio beendet Podcast Interview mit Scott Meyers bezüglich C ++ 0x. Die meisten neuen Funktionen haben für mich einen Sinn ergeben, und ich bin jetzt wirklich begeistert von C ++ 0x, mit Ausnahme von einem. Ich verstehe immer noch nicht Semantik verschieben... Was genau sind sie?


1374
2018-06-23 22:46


Ursprung


Antworten:


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

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

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

Da wir uns entschieden haben, die Erinnerung selbst zu verwalten, müssen wir der Regel von drei. Ich werde das Schreiben des Zuweisungsoperators verzögern und den Destruktor und den Kopierkonstruktor nur für den Augenblick implementieren:

    ~string()
    {
        delete[] data;
    }

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

Der Kopierkonstruktor definiert, was es bedeutet, Stringobjekte zu kopieren. Der Parameter const string& that bindet an alle Ausdrücke vom Typ string, die es Ihnen ermöglichen, in den folgenden Beispielen Kopien zu erstellen:

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

Jetzt kommt der entscheidende Einblick in die Bewegungssemantik. Beachten Sie, dass nur in der ersten Zeile, wo wir kopieren x ist diese tiefe Kopie wirklich notwendig, weil wir vielleicht inspizieren wollen x später und wäre sehr überrascht, wenn x hatte sich irgendwie verändert. Ist dir aufgefallen, wie ich gerade gesagt habe? x dreimal (viermal, wenn Sie diesen Satz einschließen) und meinte die genau dasselbe Objekt jedes Mal? Wir nennen Ausdrücke wie x "Lvalues".

Die Argumente in den Zeilen 2 und 3 sind keine Lvalues, sondern Rvalues, da die zugrundeliegenden String-Objekte keine Namen haben, so dass der Client sie zu einem späteren Zeitpunkt nicht mehr überprüfen kann. rvalues ​​bezeichnen temporäre Objekte, die beim nächsten Semikolon zerstört werden (genauer: am Ende des Full-Ausdrucks, der den rvalue lexikalisch enthält). Dies ist wichtig, weil während der Initialisierung von b und ckönnten wir mit der Quellzeichenfolge machen, was wir wollten, und Der Kunde konnte keinen Unterschied feststellen!

C ++ 0x führt einen neuen Mechanismus namens "rvalue reference" ein, der unter anderem erlaubt uns, Rvalue-Argumente über Funktionsüberladung zu erkennen. Alles, was wir tun müssen, ist einen Konstruktor mit einem R-Wert Referenzparameter schreiben. In diesem Konstruktor können wir machen alles was wir wollen mit der Quelle, solange wir es verlassen etwas gültiger Zustand:

    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 tief zu kopieren, haben wir nur den Zeiger kopiert und dann den ursprünglichen Zeiger auf Null gesetzt. In der Tat haben wir die Daten "gestohlen", die ursprünglich zum Quellstring gehörten. Die wichtigste Erkenntnis ist wiederum, dass der Kunde unter keinen Umständen feststellen konnte, dass die Quelle geändert wurde. Da wir hier keine Kopie machen, nennen wir diesen Konstruktor einen "move constructor". Seine Aufgabe besteht darin, Ressourcen von einem Objekt auf ein anderes zu verschieben, anstatt sie zu kopieren.

Herzlichen Glückwunsch, Sie verstehen jetzt die Grundlagen der Bewegungssemantik! Lassen Sie uns mit der Implementierung des Zuweisungsoperators fortfahren. Wenn Sie nicht mit dem vertraut sind Idiom kopieren und austauschen, lerne es und komm zurück, denn es ist ein fantastisches C ++ - Idiom in Bezug auf Ausnahmesicherheit.

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

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

Beachten Sie, dass wir den Parameter übergeben that  nach Wert, damit that muss wie jedes andere String-Objekt initialisiert werden. Genau wie es ist that wird initialisiert? In den alten Tagen von C ++ 98, die Antwort wäre "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 Lvalue oder ein Rvalue ist.

Also wenn du sagst a = b, das Konstruktor kopieren wird initialisiert that (weil der Ausdruck b ist ein Lvalue), und der Zuweisungsoperator tauscht den Inhalt mit einer neu erstellten, tiefen Kopie. Das ist die Definition des Copy- und Swap-Idioms - machen Sie eine Kopie, tauschen Sie den Inhalt mit der Kopie aus und entfernen Sie dann die Kopie, indem Sie den Bereich verlassen. Nichts Neues hier.

Aber wenn du sagst a = x + y, das Konstruktor verschieben wird initialisiert that (weil der Ausdruck x + y ist ein rvalue), also gibt es keine tiefe Kopie, nur eine effiziente Bewegung. that ist immer noch ein unabhängiges Objekt von dem Argument, aber seine Konstruktion war trivial, Da die Heap-Daten nicht kopiert werden mussten, wurde nur verschoben. Es war nicht notwendig, es zu kopieren, weil x + y ist ein rvalue, und wieder ist es in Ordnung, sich von String-Objekten zu entfernen, die mit rvalues ​​gekennzeichnet sind.

Zusammenfassend erstellt der Kopierkonstruktor eine tiefe Kopie, da die Quelle unberührt bleiben muss. Der Verschiebungskonstruktor hingegen kann nur den Zeiger kopieren und dann den Zeiger in der Quelle auf Null setzen. Es ist in Ordnung, das Quellenobjekt auf diese Weise zu "annullieren", da der Client keine Möglichkeit hat, das Objekt erneut zu untersuchen.

Ich hoffe, dieses Beispiel hat den Kernpunkt erreicht. Es gibt viel mehr zu rvalue Referenzen und move Semantik, die ich absichtlich weggelassen habe, um es einfach zu halten. Wenn Sie mehr Details wünschen, sehen Sie bitte meine ergänzende Antwort.


2034
2018-06-24 12:40



Meine erste Antwort war eine extrem vereinfachte Einführung in die Semantik, und viele Details wurden absichtlich weggelassen, um es einfach zu halten. Es gibt jedoch viel mehr, um die Semantik zu verschieben, und ich dachte, es wäre Zeit für eine zweite Antwort, um die Lücken zu schließen. 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, dass es immer noch gut als erste Einführung dient. Aber wenn du tiefer graben willst, lies weiter :)

Stephan T. Lavavej hat sich die Zeit genommen, wertvolle Rückmeldungen zu geben. Vielen Dank, Stephan!

Einführung

Verschiebungssemantik ermöglicht einem Objekt unter bestimmten Bedingungen, Besitz von externen Ressourcen eines anderen Objekts zu übernehmen. Dies ist in zweierlei Hinsicht wichtig:

  1. Aus teuren Kopien billige Moves machen. Siehe meine erste Antwort für ein Beispiel. Beachten Sie, dass, wenn ein Objekt nicht mindestens eine externe Ressource verwaltet (entweder direkt oder indirekt über seine Member-Objekte), die Verschieben-Semantik keine Vorteile gegenüber der Kopier-Semantik bietet. 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 von sicheren "Nur-Bewegung" -Typen; das heißt, Typen, für die Kopieren nicht sinnvoll ist, sondern Verschieben. Beispiele hierfür sind Sperren, Dateihandles und Smart Pointer mit eindeutiger Besitz-Semantik. Hinweis: In dieser Antwort wird erläutert std::auto_ptr, eine veraltete C ++ 98 - Standardbibliotheksvorlage, die durch ersetzt wurde std::unique_ptr in C ++ 11. Intermediate C ++ Programmierer sind sich wahrscheinlich zumindest einigermaßen damit vertraut std::auto_ptrund aufgrund der "move semantics", die es anzeigt, scheint es ein guter Ausgangspunkt für die Diskussion der Bewegungssemantik in C ++ 11 zu sein. YMMV.

Was ist ein Umzug?

Die C ++ 98-Standardbibliothek bietet einen intelligenten Zeiger mit eindeutiger Besitz-Semantik an std::auto_ptr<T>. Falls Sie nicht vertraut sind auto_ptrEs soll sicherstellen, dass ein dynamisch zugewiesenes Objekt immer freigegeben wird, selbst wenn Ausnahmen vorliegen:

{
    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_ptr ist sein "Kopier" -Verhalten:

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 von b mit a tut nicht kopiere das Dreieck, aber übertrage stattdessen das Eigentum an dem Dreieck a zu b. Wir sagen auch "a ist eingezogen  b"oder" das Dreieck ist gerührt von a  zu  b". Das mag verwirrend klingen, weil das Dreieck selbst immer an der gleichen Stelle im Speicher bleibt.

Das Verschieben eines Objekts bedeutet, das Eigentum einer Ressource, die es verwaltet, auf ein anderes Objekt zu übertragen.

Der Kopierkonstruktor von auto_ptr sieht wahrscheinlich 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_ptr Ist das, was syntaktisch wie eine Kopie aussieht, tatsächlich ein Zug? Versuchen, eine Mitgliedsfunktion auf einem verschobenen Objekt aufzurufen auto_ptr ruft ein undefiniertes Verhalten auf, so dass Sie sehr vorsichtig sein müssen, kein auto_ptr nachdem es 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

Aber auto_ptr ist nicht immer gefährlich. Factory-Funktionen sind ein perfektes Anwendungsbeispiel 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, dass beide Beispiele demselben syntaktischen Muster folgen:

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

Und doch ruft einer von ihnen undefiniertes Verhalten auf, während der andere dies nicht tut. Was ist der Unterschied zwischen den Ausdrücken? a und make_triangle()? Sind sie nicht beide vom selben Typ? In der Tat sind sie, aber sie haben unterschiedliche Wertkategorien.

Wertkategorien

Offensichtlich muss ein tiefer Unterschied zwischen dem Ausdruck bestehen a was bezeichnet ein auto_ptr Variable und der Ausdruck make_triangle() was den Aufruf einer Funktion bezeichnet, die ein zurückgibt auto_ptr nach Wert, so dass eine frische temporäre entsteht auto_ptr Objekt jedes Mal, wenn es aufgerufen wird. a ist ein Beispiel für ein Wert, wohingegen make_triangle() ist ein Beispiel für ein rvalue.

Von Werten wie z a Das ist gefährlich, weil wir später versuchen könnten, eine Mitgliederfunktion aufzurufen a, undefiniertes Verhalten aufrufen. Auf der anderen Seite bewegt sich von rvalues ​​wie make_triangle() ist vollkommen sicher, denn nachdem der Kopierkonstrukteur seine Arbeit erledigt hat, können wir das temporäre nicht mehr verwenden. Es gibt keinen Ausdruck, der das Temporäre bezeichnet; wenn wir einfach schreiben make_triangle()wieder, wir bekommen ein anders vorübergehend. In der Tat ist das verschobene temporäre Objekt 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 l und r einen historischen Ursprung in der linken und rechten Seite eines Auftrags haben. Dies trifft in C ++ nicht mehr zu, da es Lvalues ​​gibt, die nicht auf der linken Seite einer Zuweisung erscheinen können (wie Arrays oder benutzerdefinierte Typen ohne Zuweisungsoperator), und es gibt rvalues, die (alle rvalues ​​von Klassentypen) mit einem Zuweisungsoperator).

Ein Rvalue des Klassentyps ist ein Ausdruck, dessen Auswertung ein temporäres Objekt erzeugt.   Unter normalen Umständen bezeichnet kein anderer Ausdruck im selben Bereich dasselbe temporäre Objekt.

Rvalue-Referenzen

Wir verstehen jetzt, dass das Bewegen von Werten potenziell gefährlich ist, aber das Verschieben von Werten ist harmlos. Wenn C ++ sprachunterstützt war, um lvalue-Argumente von rvalue-Argumenten zu unterscheiden, konnten wir das Verschieben von lvalues ​​entweder ganz verbieten oder zumindest das Verschieben von lvalues ​​vornehmen explizit auf Abruf, so dass wir uns nicht mehr aus Versehen bewegen.

C ++ 11's Antwort auf dieses Problem ist rvalue Referenzen. Eine Rvalue-Referenz ist eine neue Art von Referenz, die nur an Rvalues ​​bindet, und die Syntax ist X&&. Die gute alte Referenz X& ist jetzt bekannt als L-Wert-Referenz. (Beachten Sie, dass X&& ist nicht ein Verweis auf eine Referenz; In C ++ gibt es keine solche Sache.)

Wenn wir werfen const In den Mix haben wir bereits vier verschiedene Arten von Referenzen. Welche Arten von Ausdrücken des Typs X können sie sich 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, von rvalues ​​zu lesen.

Eine rvalue Referenz X&& ist eine neue Art von Referenz, die nur an rvalues ​​bindet.

Implizite Conversions

Rvalue-Referenzen haben mehrere Versionen durchlaufen. Seit Version 2.1 ein rvalue Verweis X&& bindet auch an alle Wertkategorien eines anderen Typs Y, vorausgesetzt es gibt eine implizite Konvertierung von Y zu X. In diesem Fall ein temporärer Typ X erstellt und die rvalue-Referenz an dieses temporäre gebunden ist:

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

some_function("hello world");

Im obigen Beispiel "hello world" ist ein Wert vom Typ const char[12]. Da gibt es eine implizite Konvertierung von const char[12] durch const char* zu std::string, ein temporärer Typ std::string ist erstellt, und r ist an das temporäre gebunden. Dies ist einer der Fälle, in denen die Unterscheidung zwischen rvalues ​​(Ausdrücken) und Provisorien (Objekten) ein wenig verschwommen ist.

Konstruktoren verschieben

Ein nützliches Beispiel für eine Funktion mit einem X&& Parameter ist der Konstruktor verschieben  X::X(X&& source). Sein Zweck besteht darin, den Besitz der verwalteten Ressource von der Quelle in das aktuelle Objekt zu übertragen.

In C ++ 11, std::auto_ptr<T> wurde ersetzt durch std::unique_ptr<T> das nutzt rvalue-Referenzen. Ich werde eine vereinfachte Version von unique_ptr. Zuerst kapseln wir einen rohen Zeiger und überladen die Operatoren -> und *, also fühlt sich unsere Klasse wie ein Zeiger an:

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

public:

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

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

Der Konstruktor übernimmt das Objekt, und der Destruktor löscht es:

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

    ~unique_ptr()
    {
        delete ptr;
    }

Jetzt kommt der interessante Teil, der Move-Konstruktor:

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

Dieser Move-Konstruktor macht genau das, was auto_ptr Kopieren Konstruktor hat, aber es kann nur mit Rvalues ​​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, weil a ist ein Lvalue, aber der Parameter unique_ptr&& source kann nur an rvalues ​​gebunden werden. Genau das wollten wir; gefährliche Züge sollten niemals implizit sein. Die dritte Zeile kompiliert gut, weil make_triangle() ist ein rvalue. Der Move-Konstruktor überträgt das Eigentumsrecht vom temporären an c. Auch das ist genau das, was wir wollten.

Der Verschiebungskonstruktor überträgt den Besitz einer verwalteten Ressource in das aktuelle Objekt.

Zuweisungsoperatoren verschieben

Das letzte fehlende Stück ist der Zuweisungsoperator. Ihre Aufgabe ist es, die alte Ressource freizugeben und die neue Ressource von ihrem Argument zu übernehmen:

    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 des Destruktors und des Verschiebungskonstruktors dupliziert. Kennen Sie das Copy-and-Swap-Idiom? Es kann auch angewendet werden, um 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;
    }
};

Nun das source ist eine Variable vom Typ unique_ptrEs wird vom Move-Konstruktor initialisiert. Das heißt, das Argument wird in den Parameter verschoben. Das Argument muss immer noch ein R-Wert sein, da der Verschiebungskonstruktor selbst über einen R-Wert-Referenzparameter verfügt. Wenn der Kontrollfluss die schließende Klammer von erreicht operator=, source geht aus dem Geltungsbereich und gibt die alte Ressource automatisch frei.

Der Zugzuweisungsoperator überträgt den Besitz einer verwalteten Ressource in das aktuelle Objekt und gibt die alte Ressource frei.   Das move-and-swap-Idiom vereinfacht die Implementierung.

Von lvalues ​​ausgehend

Manchmal möchten wir von Werten abweichen. Das heißt, manchmal möchten wir, dass der Compiler einen Lvalue behandelt, als ob es ein Rvalue wäre, so dass er den Move-Konstruktor aufrufen kann, obwohl er potenziell unsicher sein könnte. Zu diesem Zweck bietet C ++ 11 eine Standardbibliotheksfunktionsvorlage namens std::move in der Kopfzeile <utility>. Dieser Name ist ein bisschen unglücklich, weil std::move einfach einen Lvalue auf einen rvalue; es tut nicht etwas selbst bewegen. Es bloß ermöglicht ziehen um. Vielleicht hätte es benannt werden sollen std::cast_to_rvalue oder std::enable_move, aber wir sind jetzt mit dem Namen festgefahren.

Hier ist, wie Sie explizit von einem Lvalue bewegen:

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 a besitzt kein Dreieck mehr. Das ist okay, weil ausdrücklich Schreiben std::move(a)Wir haben unsere Absichten klar gemacht: "Lieber Konstrukteur, mach was du willst mit a um zu initialisieren c; Es interessiert mich nicht a nicht mehr. Fühlen Sie sich frei, Ihren Weg zu finden a. "

std::move(some_lvalue) wandelt einen Wert in einen Wert um, der eine nachfolgende Bewegung ermöglicht.

Xvalues

Beachten Sie, dass obwohl std::move(a) ist ein rvalue, seine Bewertung tut es nicht Erstellen Sie ein temporäres Objekt. Dieses Problem zwang den Ausschuss, eine dritte Kategorie einzuführen. Etwas, das an eine rvalue-Referenz gebunden werden kann, obwohl es im traditionellen Sinn kein rvalue ist, wird als angerufen xwert (eXpiring Wert). Die traditionellen Rvalues ​​wurden umbenannt in Prvalues (Pure rvalues).

Sowohl prvalues ​​als auch xvalues ​​sind rvalues. Xvalues ​​und lvalues ​​sind beide glvalues (Generalisierte Werte). Die Beziehungen sind mit einem Diagramm leichter zu erfassen:

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

Beachten Sie, dass nur xvalues ​​wirklich neu sind; der Rest ist nur wegen der Umbenennung und Gruppierung.

C ++ 98 rvalues ​​sind in C ++ 11 als prvalues ​​bekannt. Ersetzen Sie alle Vorkommen von "rvalue" in den vorhergehenden Absätzen durch "prvalue".

Auszug von Funktionen

Bis jetzt haben wir Bewegung in lokale Variablen und in Funktionsparameter gesehen. Aber auch in umgekehrter Richtung ist Bewegung möglich. Wenn eine Funktion als Wert zurückgegeben wird, wird ein Objekt an der Aufrufstelle (wahrscheinlich eine lokale Variable oder ein temporäres Objekt, das jedoch ein beliebiges Objekt sein kann) mit dem Ausdruck nach dem Objekt initialisiert return Anweisung als Argument für den Move-Konstruktor:

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

Vielleicht überraschend, automatische Objekte (lokale Variablen, die nicht als deklariert sind static) kann auch sein implizit ausgezogen von Funktionen:

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

Wie kommt es, dass der Move-Konstruktor den Lvalue akzeptiert? result als Argument? Der Umfang von result ist im Begriff zu enden, und es wird beim Abwickeln des Stapels zerstört werden. Niemand könnte sich danach beschweren result hatte sich irgendwie verändert; wenn der Kontrollfluss zurück beim Anrufer ist, result existiert nicht mehr! Aus diesem Grund hat C ++ 11 eine spezielle Regel, die es erlaubt, automatische Objekte von Funktionen zurückzugeben, ohne schreiben zu müssen std::move. Eigentlich solltest du es tun noch nie benutzen std::move um automatische Objekte aus Funktionen zu entfernen, da dies die "Named Return Value Optimization" (NRVO) verhindert.

Benutze niemals std::move um automatische Objekte aus Funktionen zu entfernen.

Beachten Sie, dass in beiden Factory-Funktionen der Rückgabetyp ein Wert und keine R-Wert-Referenz ist. Rvalue-Referenzen sind immer noch Referenzen, und wie immer sollten Sie niemals einen Verweis auf ein automatisches Objekt zurückgeben; Der Aufrufer würde mit einer falschen Referenz enden, wenn Sie den Compiler dazu gebracht haben, 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 nach R-Wert-Referenz zurück. Das Verschieben wird ausschließlich vom Move-Konstruktor ausgeführt, nicht von std::moveund nicht einfach einen rvalue an eine rvalue-Referenz binden.

In Mitglieder wechseln

Früher oder später wirst du einen solchen Code schreiben:

class Foo
{
    unique_ptr<Shape> member;

public:

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

Im Grunde wird der Compiler das beschweren parameter ist ein Wert. Wenn Sie sich den Typ ansehen, sehen Sie eine rvalue-Referenz, aber eine rvalue-Referenz bedeutet einfach "eine Referenz, die an einen rvalue gebunden ist"; es tut nicht bedeutet, dass die Referenz selbst ein rvalue ist! Tatsächlich, parameter ist nur eine gewöhnliche Variable mit einem Namen. Sie können verwenden parameter so oft wie Sie möchten innerhalb des Körpers des Konstruktors, und es bezeichnet immer das gleiche Objekt. Implizit davon abzukommen wäre gefährlich, daher verbietet die Sprache es.

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
    {}
};

Das könnte man bestreiten parameter wird nach der Initialisierung von nicht mehr verwendet member. Warum gibt es keine spezielle Regel, die automatisch eingefügt wird? std::move genau wie bei Rückgabewerten? Wahrscheinlich, weil es die Compilerimplementierungen zu sehr belasten würde. Was zum Beispiel, wenn der Konstruktor in einer anderen Übersetzungseinheit war? Im Gegensatz dazu muss die Rückgabewertregel einfach die Symboltabellen überprüfen, um zu bestimmen, ob der Bezeichner nach dem return Schlüsselwort bezeichnet ein automatisches Objekt.

Sie können auch bestehen parameter nach Wert. Für Nur-Bewegungstypen wie unique_ptrEs scheint, dass es noch kein etabliertes Idiom gibt. Persönlich bevorzuge ich die Weitergabe nach Wert, da es weniger Störungen in der Schnittstelle verursacht.

Spezielle Mitgliederfunktionen

C ++ 98 deklariert implizit drei spezielle Member-Funktionen bei Bedarf, das heißt, wenn sie irgendwo benötigt werden: der Copy-Konstruktor, der Copy-Assignment-Operator und der Destruktor.

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

Rvalue-Referenzen haben mehrere Versionen durchlaufen. Seit Version 3.0 deklariert C ++ 11 bei Bedarf zwei zusätzliche spezielle Memberfunktionen: den move-Konstruktor und den move-Zuweisungsoperator. Beachten Sie, dass weder VC10 noch VC11 der Version 3.0 entsprechen. Sie müssen sie daher selbst implementieren.

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

Diese beiden neuen speziellen Memberfunktionen werden nur implizit deklariert, wenn keine der speziellen Memberfunktionen manuell deklariert wird. Wenn Sie Ihren eigenen move-Konstruktor oder move-Zuweisungsoperator 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 Memberfunktionen selbst deklarieren, und Sie erhalten eine korrekte Kopiesemantik und eine freie Semantik. Andernfalls müssen Sie die speziellen Mitgliederfunktionen selbst implementieren. Wenn Ihre Klasse nicht von der Verschiebungssemantik profitiert, müssen natürlich die speziellen Verschiebeoperationen nicht implementiert werden.

Beachten Sie, dass der Kopierzuweisungsoperator und der Verschiebungszuweisungsoperator zu einem einzigen, einheitlichen Zuweisungsoperator zusammengefasst werden können, der sein Argument nach Wert nimmt:

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. Es gibt hier einen Kompromiss zwischen Ausnahmesicherheit und Effizienz, aber ich bin kein Experte in diesem Bereich.

Weiterleitungsreferenzen (vorher bekannt als Universalreferenzen)

Betrachten Sie die folgende Funktionsvorlage:

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

Du könntest erwarten T&& nur an rvalues ​​zu binden, weil es auf den ersten Blick wie eine rvalue-Referenz aussieht. Wie sich herausstellt, T&& bindet auch an lvalues:

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 rvalue des Typs ist X, T wird abgeleitet zu sein Xdaher T&& meint X&&. Das ist, was jeder erwarten würde. Aber wenn das Argument ein lvalue des Typs ist X, aufgrund einer besonderen Regel, T wird abgeleitet zu sein X&daher T&& würde so etwas bedeuten X& &&. Aber da C ++ immer noch keine Vorstellung von Verweisen auf Referenzen hat, ist der Typ X& && ist zusammengebrochen in X&. Dies mag auf den ersten Blick verwirrend und nutzlos erscheinen, aber Referenz-Kollaps ist wesentlich für perfekte Weiterleitung (was hier nicht besprochen wird).

T && ist keine rvalue-Referenz, sondern eine Weiterleitungsreferenz. Es bindet auch an lvalues, in diesem Fall T und T&& sind beide lvalue Referenzen.

Wenn Sie eine Funktionsvorlage auf rvalues ​​beschränken möchten, können Sie kombinieren SFINAE mit Typeigenschaften:

#include <type_traits>

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

Implementierung von Umzug

Jetzt, wo Sie das Kollabieren von Referenz verstehen, hier ist wie std::move ist implementiert:

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

Wie du siehst, move akzeptiert jede Art von Parameter dank der Weiterleitungsreferenz T&&, und es gibt eine rvalue-Referenz zurück. Das std::remove_reference<T>::type Meta-Funktionsaufruf ist notwendig, da sonst für Lvalues ​​des Typs X, wäre der Rückgabetyp X& &&, die in kollabieren würde X&. Schon seit t ist immer ein Lvalue (denken Sie daran, dass eine benannte Rvalue-Referenz ein Lvalue ist), aber wir wollen binden t zu einer rvalue-Referenz müssen wir explizit casten t zum richtigen Rückgabetyp. Der Aufruf einer Funktion, die eine rvalue-Referenz zurückgibt, ist selbst ein xvalue. Jetzt weißt du wo xvalues ​​herkommen;)

Der Aufruf einer Funktion, die eine rvalue-Referenz zurückgibt, wie z std::move, ist ein xvalue.

Beachten Sie, dass in diesem Beispiel die Rückgabe durch den Wert rvalue in Ordnung ist, weil t bezeichnet kein automatisches Objekt, sondern ein Objekt, das vom Aufrufer übergeben wurde.


891
2017-07-18 11:24



Bewegungssemantik basiert auf rvalue Referenzen.
Ein rvalue ist ein temporäres Objekt, das am Ende des Ausdrucks zerstört wird. Im aktuellen C ++ binden rvalues ​​nur an const Verweise. C ++ 1x erlaubtconst rvalue Referenzen, buchstabiert T&&, die Verweise auf ein rvalue Objekte sind.
Da ein Rvalue am Ende eines Ausdrucks stirbt, können Sie klaue seine Daten. Anstatt von Kopieren es in ein anderes Objekt, du Bewegung seine 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

Im obigen Code, mit alten Compilern das Ergebnis von f() ist kopiert in x verwenden XKopieren Sie den Konstruktor. Wenn Ihr Compiler die Bewegungssemantik unterstützt und X hat einen Move-Konstruktor, dann wird dieser aufgerufen. Seit seiner rhs Argument ist ein rvalueWir wissen, dass es nicht mehr benötigt wird und wir können seinen Wert stehlen.
Also der Wert ist gerührt von dem unbenannten temporären zurückgekehrt von f() zu x (während die Daten von x, initialisiert zu einem leeren X, wird in das temporäre verschoben, das nach dem Auftrag zerstört wird).


67
2018-06-23 23:12



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(), rufen Sie den Kopierkonstruktor auf, um ihn zu initialisieren rund dann den temporären Rückgabewert zerstören. Verschiebe Semantik in C ++ 0x erlaube, dass der "move constructor" zur Initialisierung aufgerufen wird rKopieren Sie den Inhalt und verwerfen Sie den temporären Wert, ohne ihn zu zerstören.

Dies ist besonders wichtig, wenn (wie vielleicht die Matrix Beispiel), das Objekt, das kopiert wird, reserviert zusätzlichen Speicher auf dem Heap, um seine interne Darstellung zu speichern. Ein Copy-Konstruktor müsste entweder eine vollständige Kopie der internen Repräsentation erstellen oder die Referenzzählung und die Copy-on-Write-Semantik interaktiv verwenden. Ein Verschiebungskonstruktor würde den Heapspeicher allein lassen und den Zeiger einfach in den Matrix Objekt.


46
2018-06-23 22:53



Wenn Sie wirklich an einer gründlichen Erklärung der Umzugs-Semantik interessiert sind, empfehle ich Ihnen, das Original zu lesen, "Ein Vorschlag zum Hinzufügen von Move Semantics Support für die C ++ - Sprache." 

Es ist sehr zugänglich und leicht zu lesen und es spricht für die Vorteile, die es bietet. Es gibt andere, aktuellere und aktuellere Artikel über die Bewegungssemantik, die verfügbar sind die WG21-Website, aber diese ist wahrscheinlich die einfachste, da sie Dinge von einer Top-Level-Ansicht aus anspricht und nicht sehr viel in die düsteren Sprachdetails eingeht.


27
2018-06-23 23:32



Semantik verschieben handelt von Übertragen von Ressourcen, anstatt sie zu kopieren wenn niemand mehr den Quellwert benötigt.

In C ++ 03 werden Objekte oft kopiert, nur um zerstört oder zugewiesen zu werden, bevor irgendein Code den Wert erneut verwendet. Wenn Sie beispielsweise einen Wert aus einer Funktion zurückgeben - es sei denn, RVO wird eingesetzt -, wird der zurückgegebene Wert in den Stack-Frame des Aufrufers kopiert, und dann wird der Bereich verlassen und zerstört. Dies ist nur eines von vielen Beispielen: siehe pass-by-value, wenn das Quellobjekt ein temporärer Algorithmus ist sort das nur Elemente neu anordnen, Neuzuweisung in vector wenn es capacity() wird überschritten usw.

Wenn solche Copy- / Destroy-Paare teuer sind, liegt es typischerweise daran, dass das Objekt eine schwergewichtige Ressource besitzt. Beispielsweise, vector<string> kann einen dynamisch zugewiesenen Speicherblock besitzen, der ein Array von string Objekte, jedes mit einem eigenen dynamischen Speicher. 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 Sie müssen alle Speicher freigeben, die Sie gerade kopiert haben. Jedoch, ziehen um ein großer vector<string> bedeutet, nur einige Zeiger (die sich auf den dynamischen Speicherblock beziehen) auf das Ziel zu kopieren und sie in der Quelle auf Null zu setzen.


21
2018-04-08 19:47



In einfachen (praktischen) Begriffen:

Das Kopieren eines Objekts bedeutet das Kopieren seiner "statischen" Mitglieder und das Aufrufen des new Operator 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; }
};

Jedoch, um Bewegung ein Objekt (ich wiederhole es in einem praktischen Gesichtspunkt) impliziert nur, die Zeiger dynamischer Objekte zu kopieren und keine neuen zu erzeugen.

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

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 nutzlos, nein? Natürlich, aber in bestimmten Situationen ist das sehr nützlich. Am offensichtlichsten ist es, wenn ich eine Funktion mit einem anonymen Objekt (temporal, rvalue object, ..., mit anderen Namen aufrufen kann):

void heavyFunction(HeavyType());

In diesem Fall wird ein anonymes Objekt erstellt, als nächstes in den Funktionsparameter kopiert und anschließend gelöscht. Also, hier ist es besser, das Objekt zu verschieben, weil Sie das anonyme Objekt nicht benötigen und Sie Zeit und Speicher sparen können.

Dies führt zum Konzept einer "rvalue" -Referenz. Sie existieren in C ++ 11 nur, um zu erkennen, ob das empfangene Objekt anonym ist oder nicht. Ich denke, Sie wissen bereits, dass ein "Wert" eine zuweisbare Einheit ist (der linke Teil der = Operator), so dass Sie einen benannten Verweis auf ein Objekt benötigen, um als Lvalue fungieren zu können. Ein rvalue ist genau das Gegenteil, ein Objekt ohne benannte Referenzen. Aus diesem Grund sind anonymes Objekt und rvalue 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, wenn ein Objekt vom Typ A sollte "kopiert" werden, erzeugt der Compiler eine lvalue-Referenz oder eine rvalue-Referenz entsprechend, ob das übergebene Objekt benannt ist oder nicht. Wenn nicht, wird Ihr Move-Konstruktor aufgerufen und Sie wissen, dass das Objekt temporär ist und Sie seine dynamischen Objekte verschieben können, anstatt sie zu kopieren, was Platz und Speicher spart.

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

Wenn Ihr Objekt komplex ist und der Destruktor andere sekundäre Effekte hat, wie das Aufrufen einer Bibliotheksfunktion, das Aufrufen anderer globaler Funktionen oder was auch immer es ist, ist es vielleicht besser, eine Bewegung mit einer Flagge 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 keinen Code eingeben) nullptr Zuweisung für jedes dynamische Mitglied) und allgemeiner.

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

Und was ist perfekte Weiterleitung? Es ist wichtig zu wissen, dass eine "rvalue-Referenz" eine Referenz auf ein benanntes Objekt im "caller's scope" ist. Aber im tatsächlichen Gültigkeitsbereich ist eine R-Wert-Referenz ein Name für ein Objekt und fungiert somit als benanntes Objekt. Wenn Sie einen Rvalue-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 a würde auf den aktuellen Parameter von kopiert werden other_function. Wenn Sie das Objekt möchten a Wird weiterhin als temporäres Objekt behandelt, sollten Sie das verwenden std::move Funktion:

other_function(std::move(a));

Mit dieser Linie std::move wird gewirkt a zu einem rvalue und other_function empfängt das Objekt als ein unbenanntes Objekt. Natürlich, wenn other_functionhat keine spezifische Überladung, um mit unbenannten Objekten zu arbeiten, diese Unterscheidung ist nicht wichtig.

Ist das perfektes Weiterleiten? Nicht, aber wir sind sehr nah dran. Perfekte Weiterleitung ist nur nützlich, um mit Vorlagen zu arbeiten, mit dem Zweck zu sagen: Wenn ich ein Objekt an eine andere Funktion übergeben muss, muss ich, wenn ich ein benanntes Objekt erhalten, das Objekt als benanntes Objekt übergeben, und wenn nicht, Ich möchte es wie ein unbenanntes Objekt weitergeben:

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

Das ist die Signatur einer prototypischen Funktion, die eine perfekte Weiterleitung verwendet, die in C ++ 11 implementiert wurde std::forward. Diese Funktion nutzt einige Regeln der Template-Instantiierung:

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

Also, wenn T ist ein lvalue Verweis auf A (T = A &), a ebenfalls (EIN& && => A &). Ob T ist ein rvalue Verweis auf A, a auch (A && && => A &&). In beiden Fällen, a ist ein benanntes Objekt im eigentlichen Umfang, aber T enthält die Informationen seines "Referenztyps" aus der Sicht des Aufruferbereichs. Diese Information (T) wird als Template-Parameter an übergeben forward und "a" wird entsprechend dem Typ von bewegt oder nicht bewegt T.


19
2017-08-18 15:57



Es ist wie die Semantik einer Kopie, aber anstatt alle Daten duplizieren zu müssen, bekommen Sie die Daten von dem Objekt, von dem sie "verschoben" werden.


17
2018-06-23 22:56