it-swarm.com.de

Was ist Bewegungssemantik?

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

1585
dicroce

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 uns entschieden haben, den Speicher selbst zu verwalten, müssen wir die Dreierregel befolgen. Ich werde das Schreiben des Zuweisungsoperators verschieben und erst einmal 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 es bedeutet, Zeichenfolgenobjekte zu kopieren. Der Parameter const string& that ist an alle Ausdrücke des Typs string gebunden, sodass 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 entscheidende Einblick in die Bewegungssemantik. Beachten Sie, dass nur in der ersten Zeile, in der wir x kopieren, diese tiefe Kopie wirklich notwendig ist, da wir x später untersuchen möchten und sehr überrascht wären, wenn x sich irgendwie geändert hätte. Haben Sie bemerkt, dass ich gerade drei Mal x gesagt habe (vier Mal, wenn Sie diesen Satz einschließen) und jedes Mal exakt dasselbe Objekt ​​gemeint habe? Wir nennen Ausdrücke wie x "lvalues".

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 daher zu einem späteren Zeitpunkt nicht erneut überprüfen kann. rWerte bezeichnen temporäre Objekte, die beim nächsten Semikolon zerstört werden (genauer: am Ende des vollständigen Ausdrucks, der lexikalisch den rWert enthält). Dies ist wichtig, da wir während der Initialisierung von b und c mit der Quellzeichenfolge tun konnten, was immer wir wollten, und der Client konnte keinen Unterschied feststellen!

C++ 0x führt einen neuen Mechanismus mit dem Namen "rvalue reference" ein, mit dem wir unter anderem rvalue-Argumente über Funktionsüberladung erkennen können. Alles was wir tun müssen, ist einen Konstruktor mit einem rWert-Referenzparameter zu schreiben. In diesem Konstruktor können wir alles was wir wollen mit der Quelle machen, solange wir sie in etwas gültigem 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 tief 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 machen, nennen wir diesen Konstruktor einen "Verschiebungskonstruktor". Ihre Aufgabe ist es, 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 mit copy and swap idiom nicht vertraut sind, lernen Sie es und kehren Sie zurück, da es sich um eine großartige C++ - Redewendung im Zusammenhang mit Ausnahmesicherheit handelt.

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

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

Beachten Sie, dass wir den Parameter thatnach Wert ​​übergeben, sodass that wie jedes andere String-Objekt initialisiert werden muss. Genau wie wird that 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 Wert oder ein Wert ist.

Wenn Sie also a = b sagen, initialisiert der Kopierkonstruktorthat (da der Ausdruck b ein l-Wert ist), und der Zuweisungsoperator tauscht den Inhalt mit einem frisch erstellte, tiefe Kopie. Dies ist die eigentliche Definition der Kopier- und Austauschsprache: Erstellen Sie eine Kopie, tauschen Sie den Inhalt mit der Kopie aus und entfernen Sie die Kopie, indem Sie den Gültigkeitsbereich verlassen. Hier gibt es nichts Neues.

Wenn Sie jedoch a = x + y sagen, initialisiert der move-Konstruktorthat (da der Ausdruck x + y ein R-Wert ist), sodass es sich nur um eine tiefe Kopie handelt ein effizienter Umzug. that ist immer noch ein vom Argument unabhängiges Objekt, 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 + y ein R-Wert ist, und es ist auch in Ordnung, von mit R-Werten bezeichneten Zeichenfolgenobjekten wegzugehen.

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

Ich hoffe, dieses Beispiel hat den Kern der Sache verdeutlicht. Es gibt noch viel mehr, um Referenzen zu bewerten und Semantik zu verschieben, was ich absichtlich ausgelassen habe, um es einfach zu halten. Wenn Sie mehr Details wünschen, sehen Sie bitte meine ergänzende Antwort .

2326
fredoverflow

Meine erste Antwort war eine extrem vereinfachte Einführung in die Bewegungssemantik, 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 ist immer noch eine gute erste Einführung. Aber wenn du tiefer graben willst, lies 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 den Besitz von externen Ressourcen eines anderen Objekts übernehmen. Dies ist in zweierlei Hinsicht wichtig:

  1. Aus teuren Kopien werden billige Moves. Siehe meine erste Antwort für ein Beispiel. 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 und 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 ergibt, das Verschieben jedoch. Beispiele hierfür sind Sperren, Dateizugriffsnummern und intelligente Zeiger mit eindeutiger Besitzersemantik. Hinweis: In dieser Antwort wird std::auto_ptr erläutert, eine veraltete C++ 98-Standardbibliotheksvorlage, die in C++ 11 durch std::unique_ptr ersetzt wurde. Fortgeschrittene C++ - Programmierer sind wahrscheinlich zumindest ein wenig mit std::auto_ptr vertraut, 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 Umzug?

Die C++ 98-Standardbibliothek bietet einen intelligenten Zeiger mit einer eindeutigen Besitzersemantik namens std::auto_ptr<T>. Falls Sie mit auto_ptr nicht vertraut sind, soll 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 an auto_ptr ist 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 von b mit anicht ​​das Dreieck kopiert, sondern den Besitz des Dreiecks von a auf b überträgt. Wir sagen auch "a ist verschoben inb" oder "das Dreieck ist verschoben von azb". Dies mag verwirrend klingen, da das Dreieck selbst immer an der gleichen 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_ptr sieht 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 an auto_ptr ist, dass das, was syntaktisch wie eine Kopie aussieht, tatsächlich ein Zug ist. Wenn Sie versuchen, eine Member-Funktion für einen auto_ptr aufzurufen, wird ein undefiniertes Verhalten ausgelöst. Daher müssen Sie sehr vorsichtig sein, um einen auto_ptr nicht zu verwenden, nachdem dieser 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 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 dem gleichen 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 also der Unterschied zwischen den Ausdrücken a und make_triangle()? Sind sie nicht beide vom selben Typ? In der Tat, aber sie haben unterschiedliche Wertkategorien.

Wertkategorien

Offensichtlich muss ein grundlegender Unterschied zwischen dem Ausdruck a, der eine auto_ptr-Variable bezeichnet, und dem Ausdruck make_triangle() bestehen, der den Aufruf einer Funktion kennzeichnet, die einen auto_ptr nach Wert zurückgibt, wodurch jedes Mal ein neues temporäres auto_ptr-Objekt erstellt wird wird genannt. a ist ein Beispiel für lvalue, während make_triangle() ein Beispiel für rvalue ist.

Das Verschieben von lWerten wie a ist gefährlich, da wir später versuchen könnten, eine Member-Funktion über a aufzurufen, wodurch undefiniertes Verhalten ausgelöst wird. Andererseits ist das Verschieben von Werten wie make_triangle() absolut sicher, da der Kopierkonstruktor seine Arbeit erledigt hat und die temporäre Datei nicht mehr verwendet werden kann. Es gibt keinen Ausdruck, der das Temporäre bezeichnet; Wenn wir einfach noch einmal make_triangle() schreiben, erhalten wir ein different ​​temporär. In der Tat ist der Umzug von temporären 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 auf der linken und rechten Seite einer Zuweisung einen historischen Ursprung haben. Dies trifft in C++ nicht mehr zu, da es I-Werte gibt, die nicht auf der linken Seite einer Zuweisung erscheinen können (wie Arrays oder benutzerdefinierte Typen ohne Zuweisungsoperator), und es gibt R-Werte, die können (alle R-Werte von Klassentypen) mit einem Zuweisungsoperator).

Ein Wert vom Typ class ist ein Ausdruck, dessen Auswertung ein temporäres Objekt erzeugt. Unter normalen Umständen bezeichnet kein anderer Ausdruck innerhalb desselben Gültigkeitsbereichs dasselbe temporäre Objekt.

Rwertreferenzen

Wir verstehen jetzt, dass das Abspringen von Werten potentiell gefährlich ist, aber das Abspringen von Werten ist harmlos. Wenn C++ Sprachunterstützung zur Unterscheidung von lvalue-Argumenten von rvalue-Argumenten hätte, könnten wir entweder das Verschieben von lvalues ​​vollständig verbieten oder zumindest das Verschieben von lvalues ​​explicit ​​am Aufrufstandort ausführen, sodass 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 ​​gebunden ist, und die Syntax lautet X&&. Die gute alte Referenz X& heißt jetzt lvalue reference. (Beachten Sie, dass X&&nicht ​​ein Verweis auf einen Verweis ist; in C++ gibt es so etwas nicht.)

Wenn wir const in die Mischung werfen, haben wir bereits vier verschiedene Arten von Referenzen. An welche Arten von Ausdrücken vom Typ X können sie gebunden werden?

            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 const X&& vergessen. Es ist nicht sehr nützlich, sich darauf beschränken zu können, aus Werten zu lesen.

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

Implizite Konvertierungen

Rvalue-Referenzen durchliefen mehrere Versionen. Seit Version 2.1 ist eine rWertreferenz X&& auch an alle Wertkategorien eines anderen Typs Y gebunden, sofern eine implizite Konvertierung von Y in X erfolgt. In diesem Fall wird eine temporäre Datei vom Typ X erstellt, und die rvalue-Referenz ist an diese temporäre Datei gebunden:

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

some_function("hello world");

Im obigen Beispiel ist "hello world" ein Wert vom Typ const char[12]. Da eine implizite Konvertierung von const char[12] durch const char* in std::string erfolgt, wird eine temporäre Datei vom Typ std::string erstellt und r an diese temporäre Datei gebunden. Dies ist einer der Fälle, in denen die Unterscheidung zwischen rWerten (Ausdrücken) und temporären Werten (Objekten) etwas unscharf ist.

Konstruktoren verschieben

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

In C++ 11 wurde std::auto_ptr<T> durch std::unique_ptr<T> ersetzt, wobei rvalue-Referenzen genutzt werden. Ich werde eine vereinfachte Version von unique_ptr entwickeln und diskutieren. Zuerst kapseln wir einen rohen Zeiger und überladen die Operatoren -> und *, sodass sich unsere Klasse wie ein Zeiger anfühlt:

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

public:

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

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

Der Konstruktor übernimmt den Besitz des Objekts 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 move-Konstruktor macht genau das, was der copy-Konstruktor von auto_ptr getan hat, aber er kann nur mit rvalues ​​versorgt 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 a ein l-Wert ist, der Parameter unique_ptr&& source jedoch nur an r-Werte gebunden werden kann. Genau das wollten wir; gefährliche Bewegungen sollten niemals implizit sein. Die dritte Zeile kompiliert ganz gut, da make_triangle() ein Wert ist. Der move-Konstruktor überträgt den Besitz von der temporären Variable an c. Auch dies ist genau das, was wir wollten.

Der move-Konstruktor überträgt den Besitz einer verwalteten Ressource in das aktuelle Objekt.

Zuweisungsoperatoren verschieben

Das letzte fehlende Teil ist der Operator für die Zugzuweisung. Seine Aufgabe ist es, die alte Ressource freizugeben und die neue Ressource aus seinem Argument zu beziehen:

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

Da source eine Variable vom Typ unique_ptr ist, wird sie vom move-Konstruktor initialisiert. Das heißt, das Argument wird in den Parameter verschoben. Das Argument muss weiterhin ein R-Wert sein, da der Move-Konstruktor selbst über einen R-Wert-Referenzparameter verfügt. Wenn der Kontrollfluss die schließende Klammer von operator= erreicht, verlässt source den Gültigkeitsbereich und gibt die alte Ressource automatisch frei.

Der Operator für die Verschiebungszuweisung überträgt den Besitz einer verwalteten Ressource auf das aktuelle Objekt und gibt die alte Ressource frei. Die Move-and-Swap-Sprache vereinfacht die Implementierung.

Von lWerten weg

Manchmal möchten wir uns von lWerten entfernen. Das heißt, manchmal möchte der Compiler, dass ein Wert so behandelt wird, als wäre er ein R-Wert, sodass er den Verschiebungskonstruktor aufrufen kann, auch wenn er möglicherweise unsicher ist. Zu diesem Zweck bietet C++ 11 eine Standardvorlage für Bibliotheksfunktionen mit dem Namen std::move im Header <utility> an. Dieser Name ist ein bisschen unglücklich, weil std::move einfach einen l-Wert in einen r-Wert umwandelt; es bewegt nicht ​​irgendetwas von selbst. Es ist nur ermöglicht ​​bewegen. Vielleicht hätte es std::cast_to_rvalue oder std::enable_move heißen sollen, aber wir sind jetzt beim Namen geblieben.

So wechseln Sie explizit von einem lWert:

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 a nach der dritten Zeile kein Dreieck mehr besitzt. Das ist in Ordnung, denn durch explizit ​​Schreiben von std::move(a) haben wir unsere Absichten klargestellt: "Lieber Konstruktor, tue mit a, was immer du willst, um c zu initialisieren; a interessiert mich nicht mehr Fühlen Sie sich frei, Ihren Weg mit a zu haben. "

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

X-Werte

Beachten Sie, dass die Auswertung von std::move(a), obwohl es sich um einen Wert handelt, not ​​ein temporäres Objekt erstellt. Dieses Rätsel zwang das Komitee, eine dritte Wertkategorie einzuführen. Etwas, das an eine R-Wert-Referenz gebunden werden kann, obwohl es kein R-Wert im herkömmlichen Sinne ist, wird x-Wert ​​(eXpiring-Wert) genannt. Die traditionellen Werte wurden in prvalues (Pure rvalues) umbenannt.

Sowohl prvalues ​​als auch xvalues ​​sind rvalues. X-Werte und l-Werte sind beide gl-Werte (Verallgemeinerte l-Werte). 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 prvalues ​​bezeichnet. Ersetzen Sie geistig alle Vorkommen von "rvalue" in den vorhergehenden Absätzen durch "prvalue".

Funktionen verlassen

Bisher haben wir Bewegungen in lokale Variablen und in Funktionsparameter gesehen. Das Bewegen ist aber auch in umgekehrter Richtung möglich. Wenn eine Funktion einen Wert zurückgibt, wird ein Objekt an der Aufrufstelle (wahrscheinlich eine lokale Variable oder ein temporäres Objekt, es kann sich jedoch um ein beliebiges Objekt handeln) mit dem Ausdruck nach der Anweisung return als Argument für den move-Konstruktor initialisiert:

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 können automatische Objekte (lokale Variablen, die nicht als static deklariert sind) auch implizit ​​aus Funktionen verschoben werden:

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

Warum akzeptiert der move-Konstruktor den Wert result als Argument? Der Gültigkeitsbereich von result ist kurz vor dem Ende und wird beim Abwickeln des Stapels zerstört. Niemand konnte sich danach beschweren, dass sich result irgendwie geändert hatte; Wenn der Kontrollfluss wieder beim Aufrufer ist, existiert result nicht mehr! Aus diesem Grund gibt es in C++ 11 eine spezielle Regel, mit der automatische Objekte aus Funktionen zurückgegeben werden können, ohne dass std::move geschrieben werden muss. Tatsächlich sollten Sie neverstd::move verwenden, um automatische Objekte aus Funktionen zu verschieben, da dies die "Named Return Value Optimization" (NRVO) verhindert.

Verwenden Sie niemals std::move, um automatische Objekte aus Funktionen zu verschieben.

Beachten Sie, dass der Rückgabetyp in beiden Werksfunktionen ein Wert und keine Referenz ist. R-Wert-Referenzen sind weiterhin Referenzen, und wie immer sollten Sie niemals eine Referenz auf ein automatisches Objekt zurückgeben. Wenn Sie den Compiler dazu verleiteten, Ihren Code zu akzeptieren, würde der Aufrufer einen Dangling Reference erhalten:

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

Niemals automatische Objekte als Referenz zurückgeben. Das Verschieben erfolgt ausschließlich durch den Konstruktor move, nicht durch std::move und nicht durch bloßes Binden eines R-Werts an eine R-Wert-Referenz.

Mitglieder werden

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 der Compiler beschweren, dass parameter ein lvalue ist. Wenn Sie sich den Typ ansehen, sehen Sie eine rWert-Referenz, aber eine rWert-Referenz bedeutet einfach "eine Referenz, die an einen rWert gebunden ist". es bedeutet nicht, dass die Referenz selbst ein Wert ist! In der Tat ist parameter nur eine gewöhnliche Variable mit einem Namen. Sie können parameter im Rumpf des Konstruktors so oft verwenden, wie Sie möchten. Dabei wird immer dasselbe Objekt angegeben. Es wäre gefährlich, sich implizit davon zu entfernen, daher verbietet es die Sprache.

Eine benannte rWert-Referenz ist wie jede andere Variable ein lWert.

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

Sie könnten argumentieren, dass parameter nach der Initialisierung von member nicht mehr verwendet wird. Warum gibt es keine spezielle Regel zum stillen Einfügen von std::move wie 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 befände? Im Gegensatz dazu muss die Rückgabewertregel lediglich die Symboltabellen überprüfen, um festzustellen, ob der Bezeichner nach dem Schlüsselwort return ein automatisches Objekt kennzeichnet.

Sie können auch parameter als Wert übergeben. Für Nur-Verschieben-Typen wie unique_ptr scheint es noch keine festgelegte Sprache zu geben. Persönlich bevorzuge ich die Übergabe nach Wert, da dies weniger Unordnung in der Benutzeroberfläche verursacht.

Spezielle Mitgliedsfunktionen

C++ 98 deklariert implizit drei spezielle Memberfunktionen nach Bedarf, 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

Rvalue-Referenzen durchliefen mehrere Versionen. Seit Version 3.0 deklariert C++ 11 bei Bedarf zwei zusätzliche spezielle Memberfunktionen: den Verschiebungskonstruktor und den Verschiebungszuweisungsoperator. Beachten Sie, dass weder VC10 noch VC11 der Version 3.0 entsprechen, sodass Sie diese 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 Memberfunktionen selbst deklarieren, und Sie erhalten die korrekte Kopiersemantik und verschieben die Semantik kostenlos. Andernfalls müssen Sie die speziellen Member-Funktionen 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 verschmolzen werden können, wobei das Argument nach Wert geordnet 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 besteht ein Kompromiss zwischen Ausnahmesicherheit und Effizienz, aber ich bin kein Experte in diesem Bereich.

Weiterleiten von Referenzen ( zuvor bekannt als niverselle Referenzen)

Betrachten Sie die folgende Funktionsvorlage:

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

Sie können erwarten, dass T&& nur an rvalues ​​bindet, da es auf den ersten Blick wie eine rvalue-Referenz aussieht. Wie sich herausstellt, bindet T&& 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 Wert vom Typ X ist, wird T als X abgeleitet, daher bedeutet T&&X&&. Das würde jeder erwarten. Wenn das Argument jedoch ein Wert vom Typ X ist, wird aufgrund einer Sonderregel T als X& abgeleitet, daher würde T&& so etwas wie X& && bedeuten. Da C++ jedoch immer noch keine Verweise auf Verweise kennt, ist der Typ X& &&eingeklappt ​​in X&. Dies mag zunächst verwirrend und nutzlos klingen, aber das Zusammenfassen von Referenzen ist für perfekte Weiterleitung (worauf hier nicht eingegangen wird) von entscheidender Bedeutung.

T && ist keine R-Wert-Referenz, sondern eine Weiterleitungsreferenz. Es ist auch an lvalues ​​gebunden. In diesem Fall sind T und T&& beide lvalue-Referenzen.

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 sich mit dem Reduzieren von Verweisen vertraut gemacht haben, wird std::move folgendermaßen implementiert:

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, akzeptiert move dank der Weiterleitungsreferenz T&& jede Art von Parameter und gibt eine rWertreferenz zurück. Der Meta-Funktionsaufruf std::remove_reference<T>::type ist erforderlich, da andernfalls für lWerte vom Typ X der Rückgabetyp X& && lautet, der in X& zusammenfällt. Da t immer ein lvalue ist (denken Sie daran, dass eine benannte rvalue-Referenz ein lvalue ist), wir jedoch t an eine rvalue-Referenz binden möchten, müssen wir t explizit in den richtigen Rückgabetyp umwandeln. Der Aufruf einer Funktion, die eine rWert-Referenz zurückgibt, ist selbst ein xWert. Jetzt weißt du woher xvalues ​​kommen;)

Der Aufruf einer Funktion, die eine R-Wert-Referenz zurückgibt, z. B. std::move, ist ein X-Wert.

Beachten Sie, dass es in diesem Beispiel in Ordnung ist, als Referenz einen Wert zurückzugeben, da t kein automatisches Objekt bezeichnet, sondern ein Objekt, das vom Aufrufer übergeben wurde.

1009
fredoverflow

Die Verschiebungssemantik basiert auf rWertreferenzen.
Ein Wert ist ein temporäres Objekt, das am Ende des Ausdrucks zerstört wird. In aktuellem C++ binden Werte nur an const Referenzen. C++ 1x erlaubt nicht-const Wertereferenzen, geschrieben T&&, die auf ein Wertobjekt verweisen.
Da ein R-Wert am Ende eines Ausdrucks stirbt, können Sieseine Daten stehlen. Anstattzu kopierenes in ein anderes Objekt, verschieben Sieseine 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 ist bei alten Compilern das Ergebnis von f()kopiert mit x in Xs Kopierkonstruktor. Wenn Ihr Compiler die Verschiebungssemantik unterstützt und X über einen Verschiebungskonstruktor verfügt, wird dieser stattdessen aufgerufen. Da das rhs -Argument einrWertist, wissen wir, dass es nicht mehr benötigt wird und wir können seinen Wert stehlen.
Der Wert ist also moved von der unbenannten temporären zurückgegebenen von f() zu x (while Die Daten von x, die zu einem leeren X initialisiert wurden, werden in das temporäre Verzeichnis verschoben, das nach der Zuweisung zerstört wird.

76
sbi

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 auf, um r zu initialisieren, und zerstört dann den temporären Rückgabewert. Mit der Verschiebungssemantik in C++ 0x kann der "Verschiebungskonstruktor" aufgerufen werden, um r durch Kopieren seines Inhalts zu initialisieren, und anschließend den temporären Wert zu verwerfen, ohne ihn zu zerstören.

Dies ist besonders wichtig, wenn (wie vielleicht im obigen Beispiel Matrix) das kopierte Objekt zusätzlichen Speicherplatz auf dem Heap reserviert, um seine interne Darstellung zu speichern. Ein Kopierkonstruktor müsste entweder eine vollständige Kopie der internen Repräsentation erstellen oder intern die Semantik für Referenzzählung und Copy-on-Write verwenden. Ein Verschiebungskonstruktor würde den Heapspeicher in Ruhe lassen und nur den Zeiger in das Objekt Matrix kopieren.

59
Greg Hewgill

Wenn Sie wirklich an einer guten, ausführlichen Erklärung der Verschiebungssemantik interessiert sind, empfehle ich Ihnen dringend, das Originaldokument 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 es ist ein hervorragendes Argument für die Vorteile, die sie bieten. Auf der WG21-Website sind andere neuere und aktuelle Artikel zur Bewegungssemantik verfügbar, aber dieser ist wahrscheinlich der einfachste, da er sich den Dingen aus der Sicht von oben und von oben nähert kommt nicht sehr auf die grobkörnigen Sprachdetails an.

30
James McNellis

Semantik verschieben handelt von Ressourcen übertragen anstatt 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 ein Code den Wert wieder verwendet. Wenn Sie beispielsweise von einer Funktion nach Wert zurückgeben, wird der von Ihnen zurückgegebene Wert in den Stack-Frame des Aufrufers kopiert und verlässt den Gültigkeitsbereich und wird zerstört. Dies ist nur eines von vielen Beispielen: siehe Pass-by-Wert, wenn das Quellobjekt ein temporäres Objekt ist, Algorithmen wie sort, die nur Elemente neu anordnen, Neuzuweisung in vector, wenn dessen capacity() überschritten wird , usw.

Wenn solche Kopier-/Zerstörungspaare teuer sind, liegt dies normalerweise daran, dass das Objekt über eine schwere Ressource verfügt. Beispielsweise kann vector<string> einen dynamisch zugewiesenen Speicherblock besitzen, der ein Array von string Objekten 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 freigeben, den Sie gerade kopiert haben. Das Verschieben eines großen vector<string> bedeutet jedoch, dass nur einige Zeiger (die sich auf den dynamischen Speicherblock beziehen) auf das Ziel kopiert und auf Null gesetzt werden in der Quelle.

26
Dave Abrahams

Einfach (praktisch) ausgedrückt:

Ein Objekt zu kopieren bedeutet, seine "statischen" Elemente zu kopieren und den Operator new für seine dynamischen Objekte aufzurufen. Richtig?

class A
{
   int i, *p;

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

Zu verschieben einem Objekt (ich wiederhole es aus praktischer Sicht) gehört 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 (temporäres, rvalue-Objekt, ..., Sie können es mit verschiedenen Namen aufrufen):

void heavyFunction(HeavyType());

In diesem Fall 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 "Wert" -Referenz. Sie existieren in C++ 11 nur, um festzustellen, ob das empfangene Objekt anonym ist oder nicht. Ich denke, Sie wissen bereits, dass ein "lvalue" eine zuweisbare Entität ist (der linke Teil des Operators =), daher benötigen Sie einen benannten Verweis auf ein Objekt, um als lvalue fungieren zu können. Ein rWert ist genau das Gegenteil, ein Objekt ohne benannte Referenzen. Aus diesem Grund sind anonymes Objekt und 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 erstellt der Compiler, wenn ein Objekt vom Typ A "kopiert" werden soll, eine Wertereferenz oder eine Wertereferenz, je nachdem, 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 können seine dynamischen Objekte verschieben, anstatt sie zu kopieren, wodurch Speicherplatz 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 Haufen) zu "verschieben". Die Unterscheidung "Verschieben"/"Kopieren", wenn ein Objekt keine dynamischen Elemente hat (direkt oder indirekt), ist irrelevant.

Wenn Ihr Objekt komplex ist und der Destruktor andere Sekundäreffekte hat, z. B. das Aufrufen einer Bibliotheksfunktion, 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 */ }
};

Daher ist Ihr Code kürzer (Sie müssen nicht für jedes dynamische Mitglied eine nullptr -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 das Objekt 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 "Wertreferenz" eine Referenz auf ein benanntes Objekt im "Gültigkeitsbereich des Aufrufers" ist. Im eigentlichen Bereich ist eine r-Wert-Referenz ein Name für ein Objekt, sodass sie als benanntes Objekt fungiert. Wenn Sie eine rWert-Referenz 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 wird in den Aktualparameter von other_function kopiert. Wenn das Objekt a weiterhin als temporäres Objekt behandelt werden soll, sollten Sie die Funktion std::move verwenden:

other_function(std::move(a));

Mit dieser Zeile setzt std::movea auf einen Wert und other_function empfängt das Objekt als unbenanntes Objekt. Wenn other_function keine spezifische Überladung aufweist, um mit unbenannten Objekten zu arbeiten, ist diese Unterscheidung natürlich nicht wichtig.

Ist das eine perfekte Weiterleitung? Nicht, aber wir stehen uns sehr nahe. Die perfekte Weiterleitung ist nur dann sinnvoll, wenn Sie mit Vorlagen arbeiten, um Folgendes 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. 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 std::forward implementiert wurde. Diese Funktion nutzt einige Regeln für die Instanziierung von Vorlagen:

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

Wenn also T eine Wertreferenz auf A (T = A &) ist, a auch (A & && = > A &). Wenn T eine Wertreferenz auf A ist, a auch (A && && => A &&). In beiden Fällen ist a ein benanntes Objekt im tatsächlichen Bereich, aber T enthält die Informationen seines "Referenztyps" aus Sicht des Aufruferbereichs. Diese Information (T) wird als Template-Parameter an forward übergeben und 'a' wird entsprechend dem Typ von T verschoben oder nicht.

23
Peregring-lk

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

19
Terry Mahaffey

Sie wissen, was eine Kopiersemantik bedeutet, oder? Dies bedeutet, dass Sie Typen haben, die kopiert werden können. Für benutzerdefinierte Typen definieren Sie dies entweder, indem Sie explizit einen Kopierkonstruktor und einen Zuweisungsoperator schreiben, oder der Compiler generiert sie implizit. Dies wird eine Kopie machen.

Die Verschiebungssemantik ist im Grunde ein benutzerdefinierter Typ mit einem Konstruktor, der eine r-Wert-Referenz (neuer Referenztyp mit && (ja, zwei kaufmännische Und-Zeichen)) verwendet, die nicht const ist. Dies wird als Verschiebungskonstruktor bezeichnet. Dasselbe gilt für den Zuweisungsoperator. Was macht ein Move-Konstruktor also, anstatt den Speicher aus dem Quellargument zu kopieren, verschiebt er den Speicher von der Quelle in das Ziel.

Wann würdest du das wollen? Nun, std :: vector ist ein Beispiel. Angenommen, Sie haben einen temporären std :: vector erstellt, und Sie geben ihn von einer Funktion zurück, die lautet:

std::vector<foo> get_foos();

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

13
snk_kid

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: eins, 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. Bei einem eingebauten Typ ist dies sehr schnell, bei großen Objekten können diese drei Kopien jedoch 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 Austauschvorgang viel schneller ist. Die move-Zuweisung kann durch Aufrufen der std :: move () -Methode aufgerufen werden.

Beim Zurückgeben eines Objekts aus einer Methode wird standardmäßig eine Kopie des lokalen Objekts und der zugehörigen Daten an einem Ort erstellt, auf den der Aufrufer zugreifen kann (da das lokale Objekt für den Aufrufer nicht verfügbar ist und nach Beendigung der Methode verschwindet). Wenn ein eingebauter Typ zurückgegeben wird, ist dieser Vorgang sehr schnell. Wenn jedoch ein großes Objekt zurückgegeben wird, kann dies lange dauern. Mit dem move-Konstruktor kann der Programmierer dieses Standardverhalten überschreiben und stattdessen die dem lokalen Objekt zugeordneten Heap-Daten "wiederverwenden", indem er das zurückgegebene Objekt auf den Aufrufer verweist, um die dem lokalen Objekt zugeordneten Heap-Daten zu speichern. Somit ist kein Kopieren erforderlich.

In Sprachen, in denen keine lokalen Objekte (dh Objekte auf dem Stapel) erstellt werden können, treten diese Arten von Problemen nicht auf, da alle Objekte auf dem Heap zugeordnet sind und immer über Verweise darauf zugegriffen wird.

8
Chris B

Um die Notwendigkeit vonmove semanticszu veranschaulichen, betrachten wir dieses Beispiel ohne Verschiebungssemantik:

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

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

Die obige Funktion verwendetAufruf nach Wert, was bedeutet, dass beim Aufruf dieser Funktion ein Objektkonstruiertsein muss von der Funktion verwendet.
Da die Funktion auchvom Wertzurückgibt, wird ein weiteres neues Objekt für den Rückgabewert erstellt:

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

Zwei neue Objekte wurden 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, umcopyden 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 eincopy -Konstruktormacht.

Es muss zuerst das Objekt initialisieren und dann alle relevanten Daten vom alten in das neue Objekt kopieren.
Abhängig von der Klasse, ist es vielleicht ein Container mit sehr vielen Daten, dann könnte das vielZeitundSpeicher bedeuten Verwendungszweck

// 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 Semantik verschieben ist es jetzt möglich, die meiste Arbeit weniger unangenehm zu machen, indem einfachdie Daten verschoben und nicht mehr kopiert werden.

// 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);
}

Das Verschieben der Daten umfasst das erneute Verknüpfen der Daten mit dem neuen Objekt. Undes findet überhaupt keine Kopie statt.

Dies wird mit einer rvalue Referenz erreicht.
Eine rvalue Referenz funktioniert ziemlich ähnlich wie eine lvalue Referenz mit einem wichtigen Unterschied:
EinerWertreferenz kann verschoben werdenund einelWertnicht.

From 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 der Kopier- als auch der Verschiebungskonstruktor angegeben sind, 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 lvalue (benanntes Objekt oder eine Funktion/ein Operator, der eine lvalue-Referenz zurückgibt). Wenn nur der Kopierkonstruktor angegeben ist, wird er von allen Argumentkategorien ausgewählt (solange er auf const verweist, da r-Werte an const-Referenzen binden können), wodurch das Kopieren des Fallbacks für das Verschieben nicht möglich ist, wenn das Verschieben nicht möglich ist. In vielen Situationen werden Move-Konstruktoren sogar dann optimiert, wenn sie beobachtbare Nebenwirkungen hervorrufen würden (siehe Kopieroptimierung). Ein Konstruktor wird als Verschiebungskonstruktor bezeichnet, wenn er eine r-Wert-Referenz als Parameter verwendet. Es ist nicht erforderlich, irgendetwas 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 vernünftigen) Fall verschieben, in dem der Parameter a ist const rWertreferenz (const T &&).

7
Andreas DM