Wann muss ein Typ in C ++ 11 nicht verschoben werden?

126

Ich war überrascht, dass dies nicht in meinen Suchergebnissen angezeigt wurde. Ich dachte, jemand hätte dies zuvor gefragt, da die Verschiebungssemantik in C ++ 11 nützlich ist:

Wann muss ich (oder ist es eine gute Idee für mich) eine Klasse in C ++ 11 unbeweglich machen?

( Andere Gründe als Kompatibilitätsprobleme mit vorhandenem Code.)

user541686
quelle
2
Boost ist immer einen Schritt voraus - "teuer zu verschieben Typen" ( boost.org/doc/libs/1_48_0/doc/html/container/move_emplace.html )
SChepurin
1
Ich denke, dies ist eine sehr gute und nützliche Frage ( +1von mir) mit einer sehr gründlichen Antwort von Herb (oder seinem Zwilling, wie es scheint ), also habe ich daraus einen FAQ-Eintrag gemacht. Wenn jemand etwas dagegen hat, mich in der Lounge anzupingen , kann dies dort besprochen werden.
sbi
1
Bewegliche AFAIK-Klassen können weiterhin in Scheiben geschnitten werden. Daher ist es sinnvoll, das Verschieben (und Kopieren) aller polymorphen Basisklassen (dh aller Basisklassen mit virtuellen Funktionen) zu verbieten.
Philipp
1
@Mehrdad: Ich sage nur, dass "T hat einen Verschiebungskonstruktor" und " T x = std::move(anotherT);legal sein" nicht gleichwertig sind. Letzteres ist eine Verschiebungsanforderung, die auf den Kopiercode zurückgreifen kann, falls T keinen Verschiebungsfaktor hat. Was bedeutet "beweglich" genau?
Sellibitze
1
@Mehrdad: Lesen Sie im Abschnitt zur C ++ - Standardbibliothek nach, was "MoveConstructible" bedeutet. Einige Iteratoren haben möglicherweise keinen Verschiebungskonstruktor, aber es ist immer noch MoveConstructible. Achten Sie auf unterschiedliche Definitionen von "beweglichen" Personen.
Sellibitze

Antworten:

110

Die Antwort von Herb (bevor sie bearbeitet wurde) gab tatsächlich ein gutes Beispiel für einen Typ, der nicht beweglich sein sollte : std::mutex.

Der native Mutex-Typ des Betriebssystems (z. B. pthread_mutex_tauf POSIX-Plattformen) ist möglicherweise nicht "standortinvariant", was bedeutet, dass die Adresse des Objekts Teil seines Werts ist. Beispielsweise kann das Betriebssystem eine Liste von Zeigern auf alle initialisierten Mutex-Objekte führen. Wenn std::mutexein Mutex-Typ des nativen Betriebssystems als Datenelement enthalten wäre und die Adresse des nativen Typs fest bleiben muss (da das Betriebssystem eine Liste von Zeigern auf seine Mutexe verwaltet), std::mutexmüsste entweder der native Mutex-Typ auf dem Heap gespeichert werden, damit er erhalten bleibt der gleiche Ort, wenn zwischen std::mutexObjekten verschoben oder die std::mutexnicht verschoben werden dürfen. Das Speichern auf dem Heap ist nicht möglich, da a std::mutexeinen constexprKonstruktor hat und für eine konstante Initialisierung (dh statische Initialisierung) geeignet sein muss, damit eine globalestd::mutexwird garantiert erstellt, bevor die Programmausführung beginnt, sodass der Konstruktor sie nicht verwenden kann new. Die einzige Möglichkeit ist std::mutexalso, unbeweglich zu sein.

Die gleiche Überlegung gilt für andere Typen, die etwas enthalten, für das eine feste Adresse erforderlich ist. Wenn die Adresse der Ressource unverändert bleiben muss, verschieben Sie sie nicht!

Es gibt ein weiteres Argument für std::mutexdas Nichtbewegen, nämlich, dass es sehr schwierig wäre, dies sicher zu tun, da Sie wissen müssen, dass niemand versucht, den Mutex zu sperren, sobald er verschoben wird. Da Mutexe einer der Bausteine ​​sind, mit denen Sie Datenrennen verhindern können, wäre es bedauerlich, wenn sie nicht selbst gegen Rennen sicher wären! Mit einem Unbeweglichen std::mutexwissen Sie, dass das einzige, was jeder daran tun kann, wenn es erstellt wurde und bevor es zerstört wurde, darin besteht, es zu sperren und zu entsperren. Diese Vorgänge sind ausdrücklich threadsicher und führen keine Datenrennen ein. Das gleiche Argument gilt für std::atomic<T>Objekte: Wenn sie nicht atomar verschoben werden könnten, wäre es nicht möglich, sie sicher zu verschieben. Möglicherweise versucht ein anderer Thread, sie aufzurufencompare_exchange_strongauf dem Objekt genau in dem Moment, in dem es bewegt wird. Ein weiterer Fall, in dem Typen nicht beweglich sein sollten, besteht darin, dass sie einfache Bausteine ​​für sicheren gleichzeitigen Code sind und die Atomizität aller Operationen auf ihnen sicherstellen müssen. Wenn der Objektwert zu irgendeinem Zeitpunkt in ein neues Objekt verschoben werden kann, müssen Sie eine atomare Variable verwenden, um jede atomare Variable zu schützen, damit Sie wissen, ob die Verwendung sicher ist oder verschoben wurde ... und eine atomare Variable zum Schutz diese atomare Variable und so weiter ...

Ich denke, ich würde verallgemeinern, um zu sagen, dass es keinen Sinn macht, ein Objekt zu verschieben, wenn es nur ein reines Erinnerungsstück ist, kein Typ, der als Halter für einen Wert oder eine Abstraktion eines Wertes fungiert. Grundlegende Typen wie " intNicht verschieben": Das Verschieben ist nur eine Kopie. Sie können die Eingeweide nicht aus einem herausreißen int, Sie können seinen Wert kopieren und dann auf Null setzen, aber es ist immer noch ein intmit einem Wert, es sind nur Bytes Speicher. Aber ein intist noch beweglichin den Sprachbegriffen, weil eine Kopie eine gültige Verschiebungsoperation ist. Wenn Sie jedoch bei nicht kopierbaren Typen den Speicher nicht verschieben möchten oder können und auch seinen Wert nicht kopieren können, ist er nicht verschiebbar. Ein Mutex oder eine atomare Variable ist ein bestimmter Speicherort (der mit speziellen Eigenschaften behandelt wird), daher ist das Verschieben nicht sinnvoll und kann auch nicht kopiert werden, sodass er nicht verschoben werden kann.

Jonathan Wakely
quelle
17
+1 Ein weniger exotisches Beispiel für etwas, das nicht verschoben werden kann, weil es eine spezielle Adresse hat, ist ein Knoten in einer gerichteten Diagrammstruktur.
Potatoswatter
3
Wie kann ich ein Objekt kopieren oder verschieben, das einen Mutex enthält, wenn der Mutex nicht kopierbar und nicht beweglich ist? (Wie eine thread sichere Klasse mit einem eigenen Mutex für die Synchronisation ...)
tr3w
4
@ tr3w, können Sie nicht, es sei denn, Sie erstellen den Mutex auf dem Heap und halten ihn über ein unique_ptr oder ähnliches
Jonathan Wakely
2
@ tr3w: Würdest du nicht einfach die gesamte Klasse außer dem Mutex-Teil verschieben?
user541686
3
@ BenVoigt, aber das neue Objekt hat einen eigenen Mutex. Ich denke, er meint benutzerdefinierte Verschiebungsoperationen, die alle Mitglieder außer dem Mutex-Mitglied verschieben. Was ist, wenn das alte Objekt abläuft? Sein Mutex läuft damit ab.
Jonathan Wakely
57

Kurze Antwort: Wenn ein Typ kopierbar ist, sollte er auch beweglich sein. Jedoch ist das Gegenteil nicht wahr: Einige Typen wie std::unique_ptrbeweglich sind , aber es macht keinen Sinn , sie zu kopieren machen; Dies sind natürlich nur bewegliche Typen.

Es folgt eine etwas längere Antwort ...

Es gibt zwei Haupttypen (unter anderem spezielle Typen wie Merkmale):

  1. Wertähnliche Typen wie intoder vector<widget>. Diese stellen Werte dar und sollten natürlich kopierbar sein. In C ++ 11 sollten Sie das Verschieben im Allgemeinen als Optimierung des Kopierens betrachten. Daher sollten alle kopierbaren Typen natürlich verschiebbar sein. Das Verschieben ist nur eine effiziente Methode zum Erstellen einer Kopie in dem häufig vorkommenden Fall, dass Sie dies nicht tun. Ich brauche das Originalobjekt nicht mehr und werde es trotzdem zerstören.

  2. Referenzähnliche Typen, die in Vererbungshierarchien vorhanden sind, z. B. Basisklassen und Klassen mit virtuellen oder geschützten Elementfunktionen. Diese werden normalerweise durch einen Zeiger oder eine Referenz gehalten, häufig ein base*oder base&, und bieten daher keine Kopierkonstruktion, um ein Schneiden zu vermeiden. Wenn Sie ein anderes Objekt wie ein vorhandenes erhalten möchten, rufen Sie normalerweise eine virtuelle Funktion wie auf clone. Diese müssen aus zwei Gründen nicht verschoben oder zugewiesen werden: Sie können nicht kopiert werden und verfügen bereits über eine noch effizientere natürliche "Verschiebungs" -Operation - Sie kopieren / verschieben einfach den Zeiger auf das Objekt und das Objekt selbst nicht müssen überhaupt an einen neuen Speicherort umziehen.

Die meisten Typen fallen in eine dieser beiden Kategorien, aber es gibt auch andere Arten von Typen, die ebenfalls nützlich sind, nur seltener. Insbesondere hier sind Typen, die das eindeutige Eigentum an einer Ressource ausdrücken, wie z. B. std::unique_ptrnatürlich Nur-Verschieben-Typen, da sie nicht wertähnlich sind (es ist nicht sinnvoll, sie zu kopieren), aber Sie verwenden sie direkt (nicht immer) per Zeiger oder Referenz) und möchten daher Objekte dieses Typs von einem Ort zum anderen verschieben.

Kräutersutter
quelle
61
Würde der echte Herb Sutter bitte aufstehen? :)
Fredoverflow
6
Ja, ich habe von einem OAuth-Google-Konto zu einem anderen gewechselt und kann mir nicht die Mühe machen, nach einer Möglichkeit zu suchen, die beiden Anmeldungen, die mir hier zur Verfügung stehen, zusammenzuführen. (Ein weiteres Argument gegen OAuth unter den viel überzeugenderen.) Ich werde das andere wahrscheinlich nicht mehr verwenden, also das, was ich jetzt für gelegentliche SO-Posts verwenden werde.
Herb Sutter
7
Ich fand das std::mutexunbeweglich, da POSIX-Mutexe nach Adresse verwendet werden.
Welpe
9
@ SChepurin: Eigentlich heißt das dann HerbOverflow.
sbi
26
Dies wird sehr positiv bewertet. Hat niemand bemerkt, dass ein Typ nur verschoben werden sollte? Was ist nicht die Frage? :)
Jonathan Wakely
18

Eigentlich habe ich beim Durchsuchen festgestellt, dass einige Typen in C ++ 11 nicht beweglich sind:

  • alle mutexArten ( recursive_mutex, timed_mutex, recursive_timed_mutex,
  • condition_variable
  • type_info
  • error_category
  • locale::facet
  • random_device
  • seed_seq
  • ios_base
  • basic_istream<charT,traits>::sentry
  • basic_ostream<charT,traits>::sentry
  • alle atomicArten
  • once_flag

Anscheinend gibt es eine Diskussion über Clang: https://groups.google.com/forum/?fromgroups=#!topic/comp.std.c++/pCO1Qqb3Xa4

billz
quelle
1
... Iteratoren sollten nicht beweglich sein?! Was warum?
user541686
Ja, ich denke, iterators / iterator adaptorssollte weg bearbeitet werden, da C ++ 11 move_iterator hat?
Billz
Okay, jetzt bin ich nur verwirrt. Sprechen Sie über Iteratoren, die ihre Ziele verschieben , oder über das Verschieben der Iteratoren selbst ?
user541686
1
So ist es auch std::reference_wrapper. Ok, die anderen scheinen tatsächlich nicht beweglich zu sein.
Christian Rau
1
Diese scheinen in drei Kategorien zu fallen: 1. Low-Level - Parallelität bezogene Typen (atomics, mutexes), 2. polymorphen Basisklassen ( ios_base, type_info, facet), 3. verschiedene seltsame Dinge ( sentry). Wahrscheinlich sind die einzigen unbeweglichen Klassen, die ein durchschnittlicher Programmierer schreibt, in der zweiten Kategorie.
Philipp
0

Ein weiterer Grund, den ich gefunden habe - Leistung. Angenommen, Sie haben eine Klasse 'a', die einen Wert enthält. Sie möchten eine Schnittstelle ausgeben, über die ein Benutzer den Wert für eine begrenzte Zeit (für einen Bereich) ändern kann.

Eine Möglichkeit, dies zu erreichen, besteht darin, ein 'Scope Guard'-Objekt von' a 'zurückzugeben, das den Wert in seinem Destruktor zurücksetzt, wie folgt:

class a 
{ 
    int value = 0;

  public:

    struct change_value_guard 
    { 
        friend a;
      private:
        change_value_guard(a& owner, int value) 
            : owner{ owner } 
        { 
            owner.value = value;
        }
        change_value_guard(change_value_guard&&) = delete;
        change_value_guard(const change_value_guard&) = delete;
      public:
        ~change_value_guard()
        {
            owner.value = 0;
        }
      private:
        a& owner;
    };

    change_value_guard changeValue(int newValue)
    { 
        return{ *this, newValue };
    }
};

int main()
{
    a a;
    {
        auto guard = a.changeValue(2);
    }
}

Wenn ich change_value_guard beweglich machen würde, müsste ich seinem Destruktor ein 'if' hinzufügen, das prüft, ob der Guard entfernt wurde - das ist ein zusätzliches Wenn und eine Auswirkung auf die Leistung.

Ja, sicher, es kann wahrscheinlich von jedem vernünftigen Optimierer optimiert werden, aber es ist trotzdem schön, dass die Sprache (dies erfordert jedoch C ++ 17, um einen nicht beweglichen Typ zurückgeben zu können, eine garantierte Kopierelision erfordert) uns nicht benötigt das zu bezahlen, wenn wir den Wächter sowieso nicht bewegen wollen, außer ihn von der Erstellungsfunktion zurückzugeben (das Prinzip, nicht für das zu bezahlen, was Sie nicht verwenden).

saarraz1
quelle