[c++] Was sind Bewegungssemantiken?



Answers

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 Smartpointer mit eindeutiger Eigentumsrechtssemantik. Hinweis: Diese Antwort behandelt std::auto_ptr , eine veraltete C ++ 98-Standardbibliotheksvorlage, die in C ++ 11 durch std::unique_ptr ersetzt wurde. Intermediate C ++ - Programmierer kennen sich wahrscheinlich zumindest etwas mit std::auto_ptr , und aufgrund der "move semantics", die sie anzeigt, scheint es ein guter Ausgangspunkt für die Diskussion der move-Semantik in C ++ 11 zu sein. YMMV.

Was ist ein Umzug?

Die C ++ 98-Standardbibliothek bietet einen Smart-Pointer mit einer eindeutigen Besitz-Semantik namens std::auto_ptr<T> . Falls Sie mit auto_ptr nicht auto_ptr , besteht sein Zweck darin, zu garantieren, dass ein dynamisch zugewiesenes Objekt immer freigegeben wird, auch im Falle von Ausnahmen:

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

Das Besondere an auto_ptr ist das "Kopierverhalten":

auto_ptr<Shape> a(new Triangle);

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

auto_ptr<Shape> b(a);

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

Beachten Sie, dass die Initialisierung von b mit a nicht das Dreieck kopiert, sondern das Eigentum des Dreiecks von a nach b überträgt. Wir sagen auch " a wird in b bewegt " oder "das Dreieck wird von a nach b bewegt ". 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 ähnlich 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. auto_ptr versuchen, eine auto_ptr für eine verschobene auto_ptr aus auto_ptr aufzurufen, wird ein undefiniertes Verhalten auto_ptr müssen Sie sehr vorsichtig sein, auto_ptr Sie ein auto_ptr nicht verwenden, nachdem es verschoben wurde:

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, 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 a der eine Variable auto_ptr bezeichnet, und dem Ausdruck make_triangle() der den Aufruf einer Funktion bezeichnet, die auto_ptr Wert auto_ptr und so bei jedem Aufruf ein auto_ptr temporäres auto_ptr Objekt erzeugt . a ist ein Beispiel für einen Lvalue , während make_triangle() ein Beispiel für einen Rvalue ist .

Der Wechsel von lvalues ​​wie a ist gefährlich, weil wir später versuchen könnten, a Memberfunktion über a undefiniertes Verhalten aufzurufen. Auf der anderen Seite ist der Wechsel von rvalues ​​wie make_triangle() völlig ungefährlich, denn nachdem der Kopierkonstruktor seine Arbeit erledigt hat, können wir das temporäre nicht mehr verwenden. Es gibt keinen Ausdruck, der das Temporäre bezeichnet; Wenn wir make_triangle() einfach erneut schreiben, erhalten wir ein anderes temporäres. 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 auf der linken Seite und auf der rechten Seite einer Zuweisung einen historischen Ursprung 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 die Verschiebung von lvalues ​​entweder vollständig verbieten oder zumindest den expliziten Wechsel von lvalues ​​an der Call-Site vornehmen, sodass wir uns nicht mehr zufällig bewegen.

Die Antwort von C ++ 11 auf dieses Problem ist rvalue references . 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 bekannt als L-Wert Referenz . (Beachten Sie, dass X&& keine Referenz auf eine Referenz ist; in C ++ gibt es keine solche Sache.)

Wenn wir const in den Mix werfen, haben wir bereits vier verschiedene Arten von Referenzen. An 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 const X&& vergessen. 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 bindet eine rvalue-Referenz X&& auch an alle Wertkategorien eines anderen Typs Y , sofern eine implizite Konvertierung von Y nach X . In diesem Fall wird ein temporäres Objekt vom Typ X erstellt, und der Verweis rvalue ist an dieses temporäre Objekt gebunden:

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

some_function("hello world");

Im obigen Beispiel ist "hello world" ein rvalue vom Typ const char[12] . Da es eine implizite Konvertierung von const char[12] über const char* in std::string , wird ein temporäres vom Typ std::string erzeugt, und r ist an dieses 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 Move-Konstruktor 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 wurde std::auto_ptr<T> durch std::unique_ptr<T> wodurch 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 * , so dass 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 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 der auto_ptr hat, aber er 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, da a ein Lvalue ist, aber der Parameter unique_ptr&& source nur an rvalues ​​gebunden werden kann. Genau das wollten wir; gefährliche Züge sollten niemals implizit sein. Die dritte Zeile kompiliert gut, weil make_triangle() ein rvalue ist. Der Verschiebungskonstruktor überträgt das Eigentumsrecht vom temporären auf 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;
    }
};

Da diese source eine Variable vom Typ unique_ptr , wird sie vom Verschiebungskonstruktor 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 Steuerfluss die schließende Klammer von operator= erreicht, verlässt die source Gültigkeitsbereich 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 Standard-Bibliotheksfunktionsvorlage namens std::move in der Kopfzeile <utility> . Dieser Name ist ein wenig unglücklich, weil std::move einfach einen Lvalue in einen rvalue umwandelt; es bewegt sich nichts von selbst. Es ermöglicht lediglich das Bewegen. Vielleicht hätte es 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 a nach der dritten Zeile kein Dreieck mehr besitzt. Das ist in Ordnung, denn durch das explizite Schreiben von std::move(a) haben wir unsere Absichten klar gemacht: "Lieber Konstruktor, mach was immer du willst mit a , um c zu initialisieren; mir ist nichts mehr wichtig dein Weg mit a . "

std::move(some_lvalue) einen Lvalue in einen rvalue um und ermöglicht so eine nachfolgende Bewegung.

Xvalues

Beachten Sie, dass obwohl std::move(a) ein rvalue ist, seine Auswertung kein temporäres Objekt erzeugt. Dieses Problem zwang den Ausschuss, eine dritte Kategorie einzuführen. Etwas, das an eine rvalue-Referenz gebunden werden kann, obwohl es im herkömmlichen Sinn kein rvalue ist, wird als xvalue (eXpiring-Wert) bezeichnet. Die traditionellen rvalues ​​wurden in prvalues (Pure rvalues) umbenannt.

Sowohl prvalues ​​als auch xvalues ​​sind rvalues. Xvalues ​​und lvalues ​​sind beide glvalues (Generalisierte lvalues). 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 der return Anweisung 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());

Überraschenderweise können automatische Objekte (lokale Variablen, die nicht als static deklariert sind) auch implizit aus Funktionen herausbewegt werden:

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 das Lvalue- result als Argument akzeptiert? Der Umfang des result ist kurz vor dem Ende und wird beim Abwickeln des Stapels zerstört. Niemand konnte sich später beschweren, dass sich das result irgendwie geändert hatte; Wenn der Kontrollfluss beim Aufrufer zurück ist, existiert das result nicht mehr! Aus diesem Grund hat C ++ 11 eine spezielle Regel, die es erlaubt, automatische Objekte von Funktionen zurückzugeben, ohne std::move schreiben zu müssen. Tatsächlich sollten Sie std::move niemals verwenden, um automatische Objekte aus Funktionen zu entfernen, da dies die "benannte Rückgabewertoptimierung" (NRVO) verhindert.

Verwenden Sie nie 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 Verschiebungskonstruktor ausgeführt, nicht von std::move , und nicht nur, indem ein rvalue an eine rvalue-Referenz gebunden wird.

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 beschweren, dass der parameter ein Lvalue ist. 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 bedeutet nicht , dass die Referenz selbst ein rvalue ist! Tatsächlich ist parameter nur eine gewöhnliche Variable mit einem Namen. Sie können den parameter beliebig oft im Rumpf des Konstruktors verwenden und er bezeichnet immer dasselbe 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
    {}
};

Sie könnten argumentieren, dass der parameter nach der Initialisierung des member nicht mehr verwendet wird. Warum gibt es keine spezielle Regel, um std::move genau so einzufügen 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 ein automatisches Objekt bezeichnet oder nicht.

Sie können parameter nach Wert übergeben. Für unique_ptr Bewegungstypen wie unique_ptr scheint es noch kein etabliertes Idiom zu geben. 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 ( previously als universelle Referenzen bekannt )

Betrachten Sie die folgende Funktionsvorlage:

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

Sie können erwarten, dass T&& nur an rvalues ​​bindet, weil es auf den ersten Blick wie ein rvalue-Verweis 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 R-Wert vom Typ X , wird T zu X , daher bedeutet T&& X&& . Das ist, was jeder erwarten würde. Aber wenn das Argument ein Lvalue des Typs X ist, wird T aufgrund einer speziellen Regel als X& , daher würde T&& etwas wie X& && bedeuten. Aber da C ++ immer noch keine Referenzen auf Referenzen X& && ist der Typ X& && in X& . Dies mag zunächst verwirrend und nutzlos klingen, aber Referenz-Kollaps ist für eine perfekte Weiterleitung unerlässlich (auf die hier nicht eingegangen wird).

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

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

#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 Referenzen verstehen, ist hier die Implementierung von std::move :

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, akzeptiert move dank der Weiterleitungsreferenz T&& beliebige Parameter und gibt eine rvalue-Referenz zurück. Der std::remove_reference<T>::type ist notwendig, weil sonst für lvalues ​​vom Typ X der Rückgabetyp X& && , der in X& . Da t immer ein lvalue ist (denken Sie daran, dass eine benannte rvalue-Referenz ein lvalue ist), aber wir wollen t an eine rvalue-Referenz binden, müssen wir t explizit in den korrekten Rückgabetyp umwandeln. 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 wie std::move zurückgibt, ist ein xvalue.

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

Question

Ich habe gerade das Software Engineering Podcast Interview mit Scott Meyers zu 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 bekomme immer noch keine Bewegungssemantik ... Was genau sind sie?




Move semantics is about transferring resources rather than copying them when nobody needs the source value anymore.

In C++03, objects are often copied, only to be destroyed or assigned-over before any code uses the value again. For example, when you return by value from a function—unless RVO kicks in—the value you're returning is copied to the caller's stack frame, and then it goes out of scope and is destroyed. This is just one of many examples: see pass-by-value when the source object is a temporary, algorithms like sort that just rearrange items, reallocation in vector when its capacity() is exceeded, etc.

When such copy/destroy pairs are expensive, it's typically because the object owns some heavyweight resource. For example, vector<string> may own a dynamically-allocated memory block containing an array of string objects, each with its own dynamic memory. Copying such an object is costly: you have to allocate new memory for each dynamically-allocated blocks in the source, and copy all the values across. Then you need deallocate all that memory you just copied. However, moving a large vector<string> means just copying a few pointers (that refer to the dynamic memory block) to the destination and zeroing them out in the source.




To illustrate the need for move semantics , let's consider this example without move semantics:

Here's a function that takes an object of type T and returns an object of the same type T :

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

The above function uses call by value which means that when this function is called an object must be constructed to be used by the function.
Because the function also returns by value , another new object is constructed for the return value:

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

Two new objects have been constructed, one of which is a temporary object that's only used for the duration of the function.

When the new object is created from the return value, the copy constructor is called to copy the contents of the temporary object to the new object b. After the function completes, the temporary object used in the function goes out of scope and is destroyed.

Now, let's consider what a copy constructor does.

It must first initialize the object, then copy all the relevant data from the old object to the new one.
Depending on the class, maybe its a container with very much data, then that could represent much time and memory usage

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

With move semantics it's now possible to make most of this work less unpleasant by simply moving the data rather than copying.

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

Moving the data involves re-associating the data with the new object. And no copy takes place at all.

This is accomplished with an rvalue reference.
An rvalue reference works pretty much like an lvalue reference with one important difference:
an rvalue reference can be moved and an lvalue cannot.

From cppreference.com :

To make strong exception guarantee possible, user-defined move constructors should not throw exceptions. In fact, standard containers typically rely on std::move_if_noexcept to choose between move and copy when container elements need to be relocated. If both copy and move constructors are provided, overload resolution selects the move constructor if the argument is an rvalue (either a prvalue such as a nameless temporary or an xvalue such as the result of std::move), and selects the copy constructor if the argument is an lvalue (named object or a function/operator returning lvalue reference). If only the copy constructor is provided, all argument categories select it (as long as it takes a reference to const, since rvalues can bind to const references), which makes copying the fallback for moving, when moving is unavailable. In many situations, move constructors are optimized out even if they would produce observable side-effects, see copy elision. A constructor is called a 'move constructor' when it takes an rvalue reference as a parameter. It is not obligated to move anything, the class is not required to have a resource to be moved and a 'move constructor' may not be able to move a resource as in the allowable (but maybe not sensible) case where the parameter is a const rvalue reference (const T&&).




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. Verschiebungssemantik in C ++ 0x ermöglicht, dass der "move constructor" aufgerufen wird, um r zu initialisieren, indem sein Inhalt kopiert wird, und dann den temporären Wert verwerfen, ohne ihn zu zerstören.

Dies ist besonders wichtig, wenn (wie im obigen Matrix Beispiel) das Objekt, das kopiert wird, dem Speicher zusätzlichen Speicher zuweist, 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 das Matrixobjekt kopieren.




It's like copy semantics, but instead of having to duplicate all of the data you get to steal the data from the object being "moved" from.




Related