c++ wiki - Was sind Bewegungssemantiken?




6 Answers

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 die Regel von drei befolgen. 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 der an alle Ausdrücke vom Typ string bindet, 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, in der wir x kopieren, diese tiefe Kopie wirklich notwendig ist, weil wir vielleicht später x inspizieren wollen und sehr überrascht wären, wenn sich x irgendwie geändert hätte. Ist dir aufgefallen, wie ich dreimal x gesagt habe (viermal, wenn du diesen Satz eingibst) und jedes Mal dasselbe Objekt gemeint hast? 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, da wir während der Initialisierung von b und c mit der Quellzeichenfolge alles machen konnten, was wir wollten, und der Client konnte keinen Unterschied feststellen !

C ++ 0x führt einen neuen Mechanismus mit dem Namen "rvalue reference" ein, der es uns unter anderem ermöglicht, Rvalue-Argumente über Funktionsüberladung zu erkennen. Alles, was wir tun müssen, ist einen Konstruktor mit einem R-Wert Referenzparameter schreiben. Innerhalb dieses Konstruktors können wir mit der Quelle alles machen, was wir wollen , solange wir es in einem gültigen Zustand belassen:

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

Was haben wir hier gemacht? Anstatt die Heap-Daten 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 Client 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 mit dem Kopieren und Tauschen von Idiomen nicht vertraut sind, lernen Sie es und kommen Sie zurück, denn es ist ein fantastisches C ++ - Idiom im Zusammenhang mit der 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 als Wert übergeben , so that er genau wie jedes andere Zeichenfolgenobjekt initialisiert werden muss. Genau wie wird that initialisiert werden? 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 Lvalue oder ein Rvalue ist.

Wenn Sie also a = b sagen, wird der Kopierkonstruktor das initialisieren (weil der Ausdruck b ein L-Wert ist), und der Zuweisungsoperator tauscht den Inhalt mit einer frisch 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 Sie a = x + y sagen, wird der Bewegungskonstruktor das initialisieren (weil der Ausdruck x + y ein rvalue ist), also ist keine tiefe Kopie beteiligt, 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, nur verschoben. Es war nicht notwendig, es zu kopieren, weil x + y ein R-Wert ist, und wiederum ist es in Ordnung, sich von String-Objekten zu entfernen, die mit rvalues ​​bezeichnet 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 Quellobjekt 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, finden Sie in meiner ergänzenden Antwort .

move assignment

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?




Die Move-Semantik 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 Referenzen. C ++ 1x erlaubt nicht konstante rvalue-Referenzen, buchstabiert T&& , die Referenzen auf ein rvalue-Objekte sind.
Da ein Rvalue am Ende eines Ausdrucks stirbt, können Sie seine Daten stehlen . Anstatt es in ein anderes Objekt zu kopieren , verschieben Sie seine Daten dorthin.

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 wird bei alten Compilern das Ergebnis von f() Hilfe von Xs Copy-Konstruktor in x kopiert . Wenn Ihr Compiler die Bewegungssemantik unterstützt und X einen Move-Konstruktor hat, wird dieser stattdessen aufgerufen. Da sein Argument rhs ein rvalue ist , wissen wir, dass es nicht mehr benötigt wird und wir seinen Wert stehlen können.
Also wird der Wert von dem unbenannten temporären von f() nach x verschoben (während die Daten von x , initialisiert auf ein leeres X , in das temporäre verschoben werden, welches nach der Zuweisung zerstört wird).




If you are really interested in a good, in-depth explanation of move semantics, I'd highly recommend reading the original paper on them, "A Proposal to Add Move Semantics Support to the C++ Language."

It's very accessible and easy to read and it makes an excellent case for the benefits that they offer. There are other more recent and up to date papers about move semantics available on the WG21 website , but this one is probably the most straightforward since it approaches things from a top-level view and doesn't get very much into the gritty language details.




In easy (practical) terms:

Copying an object means copying its "static" members and calling the new operator for its dynamic objects. Recht?

class A
{
   int i, *p;

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

However, to move an object (I repeat, in a practical point of view) implies only to copy the pointers of dynamic objects, and not to create new ones.

But, is that not dangerous? Of course, you could destruct a dynamic object twice (segmentation fault). So, to avoid that, you should "invalidate" the source pointers to avoid destructing them twice:

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, but if I move an object, the source object becomes useless, no? Of course, but in certain situations that's very useful. The most evident one is when I call a function with an anonymous object (temporal, rvalue object, ..., you can call it with different names):

void heavyFunction(HeavyType());

In that situation, an anonymous object is created, next copied to the function parameter, and afterwards deleted. So, here it is better to move the object, because you don't need the anonymous object and you can save time and memory.

This leads to the concept of an "rvalue" reference. They exist in C++11 only to detect if the received object is anonymous or not. I think you do already know that an "lvalue" is an assignable entity (the left part of the = operator), so you need a named reference to an object to be capable to act as an lvalue. A rvalue is exactly the opposite, an object with no named references. Because of that, anonymous object and rvalue are synonyms. 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 this case, when an object of type A should be "copied", the compiler creates a lvalue reference or a rvalue reference according to if the passed object is named or not. When not, your move-constructor is called and you know the object is temporal and you can move its dynamic objects instead of copying them, saving space and memory.

It is important to remember that "static" objects are always copied. There's no ways to "move" a static object (object in stack and not on heap). So, the distinction "move"/ "copy" when an object has no dynamic members (directly or indirectly) is irrelevant.

If your object is complex and the destructor has other secondary effects, like calling to a library's function, calling to other global functions or whatever it is, perhaps is better to signal a movement with a flag:

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

So, your code is shorter (you don't need to do a nullptr assignment for each dynamic member) and more general.

Other typical question: what is the difference between A&& and const A&& ? Of course, in the first case, you can modify the object and in the second not, but, practical meaning? In the second case, you can't modify it, so you have no ways to invalidate the object (except with a mutable flag or something like that), and there is no practical difference to a copy constructor.

And what is perfect forwarding ? It is important to know that a "rvalue reference" is a reference to a named object in the "caller's scope". But in the actual scope, a rvalue reference is a name to an object, so, it acts as a named object. If you pass an rvalue reference to another function, you are passing a named object, so, the object isn't received like a temporal object.

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

The object a would be copied to the actual parameter of other_function . If you want the object a continues being treated as a temporary object, you should use the std::move function:

other_function(std::move(a));

With this line, std::move will cast a to an rvalue and other_function will receive the object as a unnamed object. Of course, if other_function has not specific overloading to work with unnamed objects, this distinction is not important.

Is that perfect forwarding? Not, but we are very close. Perfect forwarding is only useful to work with templates, with the purpose to say: if I need to pass an object to another function, I need that if I receive a named object, the object is passed as a named object, and when not, I want to pass it like a unnamed object:

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

That's the signature of a prototypical function that uses perfect forwarding, implemented in C++11 by means of std::forward . This function exploits some rules of template instantiation:

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

So, if T is a lvalue reference to A ( T = A&), a also ( A& && => A&). If T is a rvalue reference to A , a also (A&& && => A&&). In both cases, a is a named object in the actual scope, but T contains the information of its "reference type" from the caller scope's point of view. This information ( T ) is passed as template parameter to forward and 'a' is moved or not according to the type of T .




You know what a copy semantics means right? it means you have types which are copyable, for user-defined types you define this either buy explicitly writing a copy constructor & assignment operator or the compiler generates them implicitly. This will do a copy.

Move semantics is basically a user-defined type with constructor that takes an r-value reference (new type of reference using && (yes two ampersands)) which is non-const, this is called a move constructor, same goes for assignment operator. So what does a move constructor do, well instead of copying memory from it's source argument it 'moves' memory from the source to the destination.

When would you want to do that? well std::vector is an example, say you created a temporary std::vector and you return it from a function say:

std::vector<foo> get_foos();

You're going to have overhead from the copy constructor when the function returns, if (and it will in C++0x) std::vector has a move constructor instead of copying it can just set it's pointers and 'move' dynamically allocated memory to the new instance. It's kind of like transfer-of-ownership semantics with std::auto_ptr.




I'm writing this to make sure I understand it properly.

Move semantics were created to avoid the unnecessary copying of large objects. Bjarne Stroustrup in his book "The C++ Programming Language" uses two examples where unnecessary copying occurs by default: one, the swapping of two large objects, and two, the returning of a large object from a method.

Swapping two large objects usually involves copying the first object to a temporary object, copying the second object to the first object, and copying the temporary object to the second object. For a built-in type, this is very fast, but for large objects these three copies could take a large amount of time. A "move assignment" allows the programmer to override the default copy behavior and instead swap references to the objects, which means that there is no copying at all and the swap operation is much faster. The move assignment can be invoked by calling the std::move() method.

Returning an object from a method by default involves making a copy of the local object and its associated data in a location which is accessible to the caller (because the local object is not accessible to the caller and disappears when the method finishes). When a built-in type is being returned, this operation is very fast, but if a large object is being returned, this could take a long time. The move constructor allows the programmer to override this default behavior and instead "reuse" the heap data associated with the local object by pointing the object being returned to the caller to heap data associated with the local object. Thus no copying is required.

In Sprachen, die das Erstellen lokaler Objekte (dh Objekte auf dem Stapel) nicht zulassen, treten diese Typen von Problemen nicht auf, da alle Objekte auf dem Heap reserviert sind und immer auf Verweis zugegriffen wird.




Related

c++ c++-faq c++11 move-semantics