Was ist std :: move () und wann sollte es verwendet werden?

654
  1. Was ist es?
  2. Was tut es?
  3. Wann sollte es verwendet werden?

Gute Links werden geschätzt.

Basilevs
quelle
42
Bjarne Stroustrup erklärt Bewegung in einer kurzen Einführung in Rvalue Referenzen
DumbCoder
12
Diese Frage bezieht sich auf std::move(T && t); Es gibt auch std::move(InputIt first, InputIt last, OutputIt d_first)einen Algorithmus, auf den sich ein Algorithmus bezieht std::copy. Ich weise darauf hin, damit andere nicht so verwirrt sind wie ich, als ich zum ersten Mal mit std::movedrei Argumenten konfrontiert wurde . en.cppreference.com/w/cpp/algorithm/move
josaphatv

Antworten:

285

Wikipedia-Seite zu C ++ 11 R-Wert-Referenzen und Verschiebungskonstruktoren

  1. In C ++ 11 können Objekte zusätzlich zu Kopierkonstruktoren Verschiebungskonstruktoren haben.
    (Zusätzlich zu den Kopierzuweisungsoperatoren gibt es Verschiebungszuweisungsoperatoren.)
  2. Der Verschiebungskonstruktor wird anstelle des Kopierkonstruktors verwendet, wenn das Objekt den Typ "rvalue-reference" ( Type &&) hat.
  3. std::move() ist eine Besetzung, die eine r-Wert-Referenz auf ein Objekt erzeugt, um das Bewegen von diesem zu ermöglichen.

Es ist eine neue C ++ - Methode, um Kopien zu vermeiden. Wenn Sie beispielsweise einen Verschiebungskonstruktor verwenden, kann a std::vectoreinfach seinen internen Zeiger auf Daten in das neue Objekt kopieren und das verschobene Objekt in einem verschobenen Zustand belassen, sodass nicht alle Daten kopiert werden. Dies wäre C ++ - gültig.

Versuchen Sie, nach Bewegungssemantik, Wert und perfekter Weiterleitung zu googeln.

Scharron
quelle
39
Für die Verschiebungssemantik muss das verschobene Objekt gültig bleiben , was kein falscher Zustand ist. (Begründung: Es muss noch zerstören, damit es funktioniert.)
GManNickG
13
@GMan: Nun, es muss sich in einem Zustand befinden, der sicher zu zerstören ist, aber AFAIK, es muss für nichts anderes verwendbar sein.
Zan Lynx
8
@ ZanLynx: Richtig. Beachten Sie, dass für die Standardbibliothek zusätzlich verschobene Objekte zuweisbar sein müssen. Dies gilt jedoch nur für Objekte, die in der stdlib verwendet werden. Dies ist keine allgemeine Anforderung.
GManNickG
24
-1 "std :: move () ist die C ++ 11-Methode zur Verwendung der Verschiebungssemantik" Bitte beheben Sie das. std::move()ist nicht die Möglichkeit, die Verschiebungssemantik zu verwenden. Die Verschiebungssemantik wird für den Programmierer transparent ausgeführt. moveEs ist nur eine Umwandlung, um einen Wert von einem Punkt zum anderen zu übergeben, wo der ursprüngliche Wert nicht mehr verwendet wird.
Manu343726
15
Ich würde weiter gehen. std::moveselbst macht "nichts" - es hat keine Nebenwirkungen. Es signalisiert dem Compiler lediglich, dass es dem Programmierer egal ist, was mit diesem Objekt passiert. Das heißt, es gibt anderen Teilen der Software die Erlaubnis , sich vom Objekt zu entfernen, es muss jedoch nicht verschoben werden. Tatsächlich muss der Empfänger einer rWert-Referenz keine Zusagen machen, was er mit den Daten tun wird oder nicht.
Aaron McDaid
240

1. "Was ist das?"

Während std::move() technisch eine Funktion ist - ich würde sagen, es ist nicht wirklich eine Funktion . Es ist eine Art Konverter zwischen den Methoden, mit denen der Compiler den Wert eines Ausdrucks berücksichtigt.

2. "Was macht es?"

Das erste, was zu beachten ist, ist, dass std::move() sich nichts bewegt . Es konvertiert einen Ausdruck von einem l-Wert (z. B. einer benannten Variablen) in einen x-Wert . Ein xvalue teilt dem Compiler mit:

Du kannst mich plündern, alles , was ich halte , bewegen und woanders verwenden (da ich sowieso bald zerstört werde) ".

Mit anderen Worten, wenn Sie verwenden std::move(x), lassen Sie den Compiler ausschlachten x. Wenn xalso beispielsweise ein eigener Puffer im Speicher vorhanden ist, std::move()kann der Compiler nach dem Erstellen stattdessen ein anderes Objekt besitzen.

Sie können auch von einem Wert wechseln (z. B. von einem temporären Wert, den Sie herumgeben ), dies ist jedoch selten nützlich.

3. "Wann sollte es verwendet werden?"

Eine andere Möglichkeit, diese Frage zu stellen, lautet: "Wofür kann ich die Ressourcen eines vorhandenen Objekts ausschlachten?" Wenn Sie Anwendungscode schreiben, werden Sie wahrscheinlich nicht viel mit temporären Objekten herumspielen, die vom Compiler erstellt wurden. Sie würden dies also hauptsächlich an Orten wie Konstruktoren, Operatormethoden, Standardbibliotheksalgorithmus-ähnlichen Funktionen usw. tun, an denen Objekte häufig automatisch erstellt und zerstört werden. Das ist natürlich nur eine Faustregel.

Eine typische Verwendung besteht darin, Ressourcen von einem Objekt zu einem anderen zu verschieben, anstatt sie zu kopieren. @ Guillaume verlinkt auf diese Seite, die ein einfaches kurzes Beispiel enthält: Vertauschen von zwei Objekten mit weniger Kopieraufwand.

template <class T>
swap(T& a, T& b) {
    T tmp(a);   // we now have two copies of a
    a = b;      // we now have two copies of b (+ discarded a copy of a)
    b = tmp;    // we now have two copies of tmp (+ discarded a copy of b)
}

Mit move können Sie die Ressourcen austauschen, anstatt sie zu kopieren:

template <class T>
swap(T& a, T& b) {
    T tmp(std::move(a));
    a = std::move(b);   
    b = std::move(tmp);
}

Denken Sie daran, was passiert, wenn Tes beispielsweise die vector<int>Größe n hat. In der ersten Version lesen und schreiben Sie 3 * n Elemente, in der zweiten Version lesen und schreiben Sie im Grunde nur die 3 Zeiger auf die Vektorpuffer plus die 3 Puffergrößen. Natürlich muss die Klasse Twissen, wie man sich bewegt; Ihre Klasse sollte einen Verschiebungszuweisungsoperator und einen Verschiebungskonstruktor für die Klasse haben, Tdamit dies funktioniert.

einpoklum
quelle
3
Ich habe lange von dieser Bewegungssemantik gehört und sie nie untersucht. Aus dieser Beschreibung geht hervor, dass es sich nur um eine flache Kopie statt um eine tiefe Kopie handelt.
Zebrafisch
7
@TitoneMaurice: Nur dass es sich nicht um eine Kopie handelt, da der ursprüngliche Wert nicht mehr verwendet werden kann.
Einpoklum
3
@Zebrafish du könntest nicht falscher sein. Eine flache Kopie belässt das Original im exakt gleichen Zustand. Eine Verschiebung führt normalerweise dazu, dass das Original leer oder in einem anderweitig gültigen Zustand ist.
Rubenvb
16
@rubenvb Zebra ist nicht ganz falsch. Zwar wird das ursprüngliche cannabilisierte Objekt normalerweise absichtlich sabotiert, um verwirrende Fehler zu vermeiden (z. B. setzen Sie seine Zeiger auf nullptr, um zu signalisieren, dass es die Zeiger nicht mehr besitzt), aber die Tatsache, dass die gesamte Bewegung durch einfaches Kopieren eines Zeigers aus der Quelle implementiert wird zum Ziel zu gelangen (und absichtlich zu vermeiden, etwas mit dem Pointee zu tun), erinnert in der Tat an eine flache Kopie. In der Tat würde ich gehen sogar so weit zu sagen , dass ein Umzug ist eine flache Kopie, gegebenenfalls gefolgt von einer teilweisen Selbstzerstörung der Quelle. (Forts.)
Leichtigkeitsrennen im Orbit
3
(Forts.) Wenn wir diese Definition zulassen (und ich mag sie eher), dann ist die Beobachtung von @ Zebrafish nicht falsch, nur etwas unvollständig.
Leichtigkeitsrennen im Orbit
144

Sie können move verwenden, wenn Sie den Inhalt eines Objekts an eine andere Stelle "übertragen" müssen, ohne eine Kopie zu erstellen (dh der Inhalt wird nicht dupliziert, daher kann er für einige nicht kopierbare Objekte wie unique_ptr verwendet werden). Mit std :: move kann ein Objekt auch den Inhalt eines temporären Objekts übernehmen, ohne eine Kopie zu erstellen (und viel Zeit zu sparen).

Dieser Link hat mir wirklich geholfen:

http://thbecker.net/articles/rvalue_references/section_01.html

Es tut mir leid, wenn meine Antwort zu spät kommt, aber ich habe auch nach einem guten Link für den std :: move gesucht und die obigen Links etwas "streng" gefunden.

Dies legt den Schwerpunkt auf die R-Wert-Referenz, in welchem ​​Kontext Sie sie verwenden sollten, und ich denke, es ist detaillierter, deshalb wollte ich diesen Link hier teilen.

Guillaume
quelle
26
Netter Link. Ich fand den Wikipedia-Artikel und andere Links, über die ich gestolpert bin, immer ziemlich verwirrend, da sie nur Fakten auf Sie werfen und es Ihnen überlassen, herauszufinden, was die tatsächliche Bedeutung / Begründung ist. Während "Verschiebungssemantik" in einem Konstruktor ziemlich offensichtlich ist, sind all diese Details zum Übergeben von && - Werten nicht ... daher war die Beschreibung im Tutorial-Stil sehr schön.
Christian Stieber
66

F: Was ist std::move?

A: std::move()ist eine Funktion aus der C ++ - Standardbibliothek zum Umwandeln in eine rvalue-Referenz.

Einfach ausgedrückt std::move(t)ist gleichbedeutend mit:

static_cast<T&&>(t);

Ein r-Wert ist ein temporärer Wert, der nicht über den Ausdruck hinaus bestehen bleibt, der ihn definiert, z. B. ein Ergebnis einer Zwischenfunktion, das niemals in einer Variablen gespeichert wird.

int a = 3; // 3 is a rvalue, does not exist after expression is evaluated
int b = a; // a is a lvalue, keeps existing after expression is evaluated

Eine Implementierung für std :: move () finden Sie in N2027: "Eine kurze Einführung in Rvalue-Referenzen" wie folgt:

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

Wie Sie sehen können, std::movekehrt T&&unabhängig davon , ob mit einem Wert (genannt T), Referenz - Typ ( T&) oder R - Wert - Referenz ( T&&).

F: Was macht es?

A: Als Cast macht es zur Laufzeit nichts. Es ist nur zur Kompilierungszeit relevant, dem Compiler mitzuteilen, dass Sie die Referenz weiterhin als Wert betrachten möchten.

foo(3 * 5); // obviously, you are calling foo with a temporary (rvalue)

int a = 3 * 5;
foo(a);     // how to tell the compiler to treat `a` as an rvalue?
foo(std::move(a)); // will call `foo(int&& a)` rather than `foo(int a)` or `foo(int& a)`

Was es nicht macht:

  • Machen Sie eine Kopie des Arguments
  • Rufen Sie den Kopierkonstruktor auf
  • Ändern Sie das Argumentobjekt

F: Wann sollte es verwendet werden?

A: Sie sollten verwenden, std::movewenn Sie Funktionen aufrufen möchten, die die Verschiebungssemantik mit einem Argument unterstützen, das kein r-Wert ist (temporärer Ausdruck).

Dies wirft für mich folgende Anschlussfragen auf:

  • Was ist Bewegungssemantik? Die Verschiebungssemantik im Gegensatz zur Kopiersemantik ist eine Programmiertechnik, bei der die Elemente eines Objekts durch "Übernehmen" initialisiert werden, anstatt die Elemente eines anderen Objekts zu kopieren. Eine solche Übernahme ist nur bei Zeigern und Ressourcenhandles sinnvoll, die kostengünstig übertragen werden können, indem der Zeiger oder das Integer-Handle anstelle der zugrunde liegenden Daten kopiert werden.

  • Welche Klassen und Objekte unterstützen die Bewegungssemantik? Es liegt an Ihnen als Entwickler, die Verschiebungssemantik in Ihren eigenen Klassen zu implementieren, wenn diese von der Übertragung ihrer Mitglieder profitieren würden, anstatt sie zu kopieren. Sobald Sie die Verschiebungssemantik implementiert haben, profitieren Sie direkt von der Arbeit vieler Bibliotheksprogrammierer, die Unterstützung für die effiziente Behandlung von Klassen mit Verschiebungssemantik hinzugefügt haben.

  • Warum kann der Compiler es nicht selbst herausfinden? Der Compiler kann nicht einfach eine weitere Überladung einer Funktion aufrufen, es sei denn, Sie sagen dies. Sie müssen dem Compiler bei der Auswahl helfen, ob die reguläre oder die Verschiebungsversion der Funktion aufgerufen werden soll.

  • In welchen Situationen möchte ich dem Compiler mitteilen, dass er eine Variable als rWert behandeln soll? Dies geschieht höchstwahrscheinlich in Vorlagen- oder Bibliotheksfunktionen, bei denen Sie wissen, dass ein Zwischenergebnis gerettet werden kann.

Christopher Oezbek
quelle
2
Big +1 für Codebeispiele mit Semantik in Kommentaren. Die anderen Top-Antworten definieren std :: move mit "move" selbst - klärt nichts wirklich! --- Ich glaube, es ist erwähnenswert, dass das Fehlen einer Kopie des Arguments bedeutet, dass der ursprüngliche Wert nicht zuverlässig verwendet werden kann.
ty
34

std :: move selbst macht nicht wirklich viel. Ich dachte, dass es den verschobenen Konstruktor für ein Objekt aufruft, aber es führt wirklich nur eine Typumwandlung durch (Umwandlung einer lvalue-Variablen in einen rvalue, damit diese Variable als Argument an einen Verschiebungskonstruktor oder einen Zuweisungsoperator übergeben werden kann).

Daher wird std :: move nur als Vorläufer für die Verwendung der Verschiebungssemantik verwendet. Die Verschiebungssemantik ist im Wesentlichen eine effiziente Methode für den Umgang mit temporären Objekten.

Betrachten Sie das Objekt A = B + C + D + E + F;

Das ist gut aussehender Code, aber E + F erzeugt ein temporäres Objekt. Dann erzeugt D + temp ein anderes temporäres Objekt und so weiter. In jedem normalen "+" - Operator einer Klasse treten tiefe Kopien auf.

Zum Beispiel

Object Object::operator+ (const Object& rhs) {
    Object temp (*this);
    // logic for adding
    return temp;
}

Die Erstellung des temporären Objekts in dieser Funktion ist nutzlos - diese temporären Objekte werden ohnehin am Ende der Zeile gelöscht, wenn sie den Gültigkeitsbereich verlassen.

Wir können lieber die Bewegungssemantik verwenden, um die temporären Objekte zu "plündern" und so etwas zu tun

 Object& Object::operator+ (Object&& rhs) {
     // logic to modify rhs directly
     return rhs;
 }

Dadurch wird vermieden, dass unnötig tiefe Kopien erstellt werden. In Bezug auf das Beispiel ist der einzige Teil, in dem tiefes Kopieren stattfindet, jetzt E + F. Der Rest verwendet die Bewegungssemantik. Der Verschiebungskonstruktor oder Zuweisungsoperator muss ebenfalls implementiert werden, um das Ergebnis A zuzuweisen.

user929404
quelle
3
Sie haben über Bewegungssemantik gesprochen. Sie sollten Ihrer Antwort hinzufügen, wie std :: move verwendet werden kann, da die Frage danach fragt.
Koushik Shetty
2
@Koushik std :: move macht nicht viel - wird aber verwendet, um die Bewegungssemantik zu implementieren. Wenn Sie nicht über std :: move Bescheid wissen, kennen Sie wahrscheinlich auch die Verschiebungssemantik nicht
user929404
1
"macht nicht viel" (ja nur ein static_cast zu einer rvalue referenz). Was es tatsächlich tut und was es tut, ist das, was das OP gefragt hat. Sie müssen nicht wissen, wie std :: move funktioniert, aber Sie müssen wissen, was die Bewegungssemantik bewirkt. Darüber hinaus ist "aber wird verwendet, um die Bewegungssemantik zu implementieren" umgekehrt. Kennen Sie die Bewegungssemantik und Sie werden std :: move verstehen, sonst nein. move hilft nur bei der Bewegung und verwendet selbst die Bewegungssemantik. std :: move konvertiert sein Argument nur in eine r-Wert-Referenz, was für die Bewegungssemantik erforderlich ist.
Koushik Shetty
10
"aber E + F erzeugt ein temporäres Objekt" - Der Operator +geht von links nach rechts, nicht von rechts nach links. Daher B+Cwäre zuerst!
Ajay
8

"Was ist es?" und "Was macht es?" wurde oben erklärt.

Ich werde ein Beispiel geben, "wann es verwendet werden sollte".

Zum Beispiel haben wir eine Klasse mit vielen Ressourcen wie einem großen Array.

class ResHeavy{ //  ResHeavy means heavy resource
    public:
        ResHeavy(int len=10):_upInt(new int[len]),_len(len){
            cout<<"default ctor"<<endl;
        }

        ResHeavy(const ResHeavy& rhs):_upInt(new int[rhs._len]),_len(rhs._len){
            cout<<"copy ctor"<<endl;
        }

        ResHeavy& operator=(const ResHeavy& rhs){
            _upInt.reset(new int[rhs._len]);
            _len = rhs._len;
            cout<<"operator= ctor"<<endl;
        }

        ResHeavy(ResHeavy&& rhs){
            _upInt = std::move(rhs._upInt);
            _len = rhs._len;
            rhs._len = 0;
            cout<<"move ctor"<<endl;
        }

    // check array valid
    bool is_up_valid(){
        return _upInt != nullptr;
    }

    private:
        std::unique_ptr<int[]> _upInt; // heavy array resource
        int _len; // length of int array
};

Testcode:

void test_std_move2(){
    ResHeavy rh; // only one int[]
    // operator rh

    // after some operator of rh, it becomes no-use
    // transform it to other object
    ResHeavy rh2 = std::move(rh); // rh becomes invalid

    // show rh, rh2 it valid
    if(rh.is_up_valid())
        cout<<"rh valid"<<endl;
    else
        cout<<"rh invalid"<<endl;

    if(rh2.is_up_valid())
        cout<<"rh2 valid"<<endl;
    else
        cout<<"rh2 invalid"<<endl;

    // new ResHeavy object, created by copy ctor
    ResHeavy rh3(rh2);  // two copy of int[]

    if(rh3.is_up_valid())
        cout<<"rh3 valid"<<endl;
    else
        cout<<"rh3 invalid"<<endl;
}

Ausgabe wie folgt:

default ctor
move ctor
rh invalid
rh2 valid
copy ctor
rh3 valid

Wir können sehen, dass sich std::movemit move constructormake die Ressource leicht transformieren lässt.

Wo sonst ist std::movenützlich?

std::movekann auch beim Sortieren eines Arrays von Elementen hilfreich sein. Viele Sortieralgorithmen (wie Auswahlsortierung und Blasensortierung) tauschen Elementpaare aus. Bisher mussten wir auf die Kopiersemantik zurückgreifen, um den Austausch durchzuführen. Jetzt können wir die Verschiebungssemantik verwenden, die effizienter ist.

Dies kann auch nützlich sein, wenn Sie den von einem intelligenten Zeiger verwalteten Inhalt auf einen anderen verschieben möchten.

Zitiert:

Jayhello
quelle
0

Hier ist ein vollständiges Beispiel mit std :: move für einen (einfachen) benutzerdefinierten Vektor

Erwartete Ausgabe:

 c: [10][11]
 copy ctor called
 copy of c: [10][11]
 move ctor called
 moved c: [10][11]

Kompilieren als:

  g++ -std=c++2a -O2 -Wall -pedantic foo.cpp

Code:

#include <iostream>
#include <algorithm>

template<class T> class MyVector {
private:
    T *data;
    size_t maxlen;
    size_t currlen;
public:
    MyVector<T> () : data (nullptr), maxlen(0), currlen(0) { }
    MyVector<T> (int maxlen) : data (new T [maxlen]), maxlen(maxlen), currlen(0) { }

    MyVector<T> (const MyVector& o) {
        std::cout << "copy ctor called" << std::endl;
        data = new T [o.maxlen];
        maxlen = o.maxlen;
        currlen = o.currlen;
        std::copy(o.data, o.data + o.maxlen, data);
    }

    MyVector<T> (const MyVector<T>&& o) {
        std::cout << "move ctor called" << std::endl;
        data = o.data;
        maxlen = o.maxlen;
        currlen = o.currlen;
    }

    void push_back (const T& i) {
        if (currlen >= maxlen) {
            maxlen *= 2;
            auto newdata = new T [maxlen];
            std::copy(data, data + currlen, newdata);
            if (data) {
                delete[] data;
            }
            data = newdata;
        }
        data[currlen++] = i;
    }

    friend std::ostream& operator<<(std::ostream &os, const MyVector<T>& o) {
        auto s = o.data;
        auto e = o.data + o.currlen;;
        while (s < e) {
            os << "[" << *s << "]";
            s++;
        }
        return os;
    }
};

int main() {
    auto c = new MyVector<int>(1);
    c->push_back(10);
    c->push_back(11);
    std::cout << "c: " << *c << std::endl;
    auto d = *c;
    std::cout << "copy of c: " << d << std::endl;
    auto e = std::move(*c);
    delete c;
    std::cout << "moved c: " << e << std::endl;
}
Neil McGill
quelle