Move-Konstruktoren
Ein Move-Konstruktor ist ein Konstruktor, der mit einem Argument desselben Klassentyps aufgerufen werden kann und den Inhalt des Arguments kopiert, wobei das Argument möglicherweise mutiert wird.
Inhalt |
[bearbeiten] Syntax
Klassenname (Parameterliste ); |
(1) | ||||||||
Klassenname (Parameterliste ) Funktionsrumpf |
(2) | ||||||||
Klassenname (Einzelparameterliste ) = default; |
(3) | ||||||||
Klassenname (Parameterliste ) = delete; |
(4) | ||||||||
Klassenname ::Klassenname (Parameterliste ) Funktionsrumpf |
(5) | ||||||||
Klassenname ::Klassenname (Einzelparameterliste ) = default; |
(6) | ||||||||
| Klassenname | - | die Klasse, deren Move-Konstruktor deklariert wird |
| parameter-liste | - | eine nicht-leere Parameterliste, die alle folgenden Bedingungen erfüllt
|
| Einzelparameterliste | - | eine Parameterliste mit nur einem Parameter, der vom Typ T&&, const T&&, volatile T&& oder const volatile T&& ist und kein Standardargument hat |
| function-body | - | der Funktionsrumpf des Move-Konstruktors |
[bearbeiten] Erklärung
struct X { X(X&& other); // move constructor // X(X other); // Error: incorrect parameter type }; union Y { Y(Y&& other, int num = 1); // move constructor with multiple parameters // Y(Y&& other, int num); // Error: `num` has no default argument };
Der Move-Konstruktor wird typischerweise aufgerufen, wenn ein Objekt initialisiert wird (durch direkte Initialisierung oder Kopierinitialisierung) aus rvalue (xvalue oder prvalue)(bis C++17)xvalue(seit C++17) desselben Typs, einschließlich
- Initialisierung: T a = std::move(b); oder T a(std::move(b));, wobei b vom Typ
Tist; - Funktionsargumentübergabe: f(std::move(a));, wobei a vom Typ
Tist und f als void f(T t) deklariert ist; - Funktionsrückgabe: return a; in einer Funktion wie T f(), wobei a vom Typ
Tist, der einen Move-Konstruktor besitzt.
Wenn der Initialisierer ein prvalue ist, wird der Aufruf des Move-Konstruktors oft optimiert(bis C++17)nie gemacht(seit C++17), siehe Kopier-Elision.
Move-Konstruktoren übertragen typischerweise die vom Argument gehaltenen Ressourcen (z.B. Zeiger auf dynamisch allokierte Objekte, Dateideskriptoren, TCP-Sockets, Thread-Handles usw.), anstatt Kopien davon zu erstellen, und hinterlassen das Argument in einem gültigen, aber ansonsten unbestimmten Zustand. Da der Move-Konstruktor die Lebensdauer des Arguments nicht ändert, wird der Destruktor typischerweise zu einem späteren Zeitpunkt für das Argument aufgerufen. Beispielsweise kann das Verschieben eines std::string oder eines std::vector dazu führen, dass das Argument leer bleibt. Für einige Typen, wie z.B. std::unique_ptr, ist der verschobene Zustand vollständig spezifiziert.
[bearbeiten] Implizit deklarierter Move-Konstruktor
Wenn für einen Klassentyp keine benutzerdefinierten Move-Konstruktoren bereitgestellt werden und alle der folgenden Bedingungen erfüllt sind
- keine benutzerdeklarierten Kopierkonstruktoren vorhanden sind;
- keine benutzerdeklarierten Kopierzuweisungsoperatoren vorhanden sind;
- keine benutzerdeklarierten Move-Zuweisungsoperatoren vorhanden sind;
- kein benutzerdeklarierter Destruktor vorhanden ist.
Dann deklariert der Compiler einen Move-Konstruktor als nicht-explizites inline public Mitglied seiner Klasse mit der Signatur T::T(T&&).
Eine Klasse kann mehrere Move-Konstruktoren haben, z.B. sowohl T::T(const T&&) als auch T::T(T&&). Wenn einige benutzerdefinierte Move-Konstruktoren vorhanden sind, kann der Benutzer die Generierung des implizit deklarierten Move-Konstruktors mit dem Schlüsselwort default erzwingen.
Der implizit deklarierte (oder bei seiner ersten Deklaration standardmäßig festgelegte) Move-Konstruktor hat eine Ausnahmespezifikation wie in dynamische Ausnahmespezifikation(bis C++17)noexcept-Spezifikation(seit C++17) beschrieben.
[bearbeiten] Implizit definierter Move-Konstruktor
Wenn der implizit deklarierte Move-Konstruktor weder gelöscht noch trivial ist, wird er (d.h. ein Funktionsrumpf wird generiert und kompiliert) vom Compiler definiert, wenn er durch ODR-benutzt oder für die konstante Auswertung benötigt wird. Für Union-Typen kopiert der implizit definierte Move-Konstruktor die Objekt-Repräsentation (wie bei std::memmove). Für Nicht-Union-Klassentypen führt der Move-Konstruktor eine vollständige elementweise Verschiebung der direkten Basis-Subobjekte und Mitgliedssubobjekte des Objekts in ihrer Initialisierungsreihenfolge durch, mittels direkter Initialisierung mit einem xvalue-Argument. Für jedes nicht-statische Datenmitglied eines Referenztyps bindet der Move-Konstruktor die Referenz an dasselbe Objekt oder dieselbe Funktion, an die die Quellreferenz gebunden ist.
Wenn dies die Anforderungen eines constexpr Konstruktors(bis C++23)constexpr Funktion(seit C++23) erfüllt, ist der generierte Move-Konstruktor constexpr.
[bearbeiten] Gelöschter Move-Konstruktor
Der implizit deklarierte oder explizit standardmäßig festgelegte Move-Konstruktor für die Klasse T wird als gelöscht definiert, wenn T ein potenziell zu konstruierendes Subobjekt vom Klassentyp M hat (oder möglicherweise ein mehrdimensionales Array davon), so dass
-
Meinen Destruktor hat, der gelöscht oder vom Kopierkonstruktor aus nicht zugänglich ist, oder - die Überladungsauflösung zur Ermittlung des Move-Konstruktors von
M
- nicht zu einem benutzbaren Kandidaten führt, oder
- im Falle des Subobjekts als Varianten-Mitglied eine nicht-triviale Funktion auswählt.
Ein solcher Konstruktor wird von der Überladungsauflösung ignoriert (andernfalls würde er die Kopierinitialisierung aus einem rvalue verhindern).
[bearbeiten] Trivialer Move-Konstruktor
Der Move-Konstruktor für die Klasse T ist trivial, wenn alle der folgenden Bedingungen erfüllt sind
- er nicht benutzerdefiniert ist (d.h., er ist implizit definiert oder standardmäßig festgelegt);
-
Tkeine virtuellen Memberfunktionen hat; -
Tkeine virtuellen Basisklassen hat; - der für jede direkte Basis von
Tausgewählte Move-Konstruktor trivial ist; - der für jedes nicht-statische Mitglied vom Klassentyp (oder Array von Klassentyp) von
Tausgewählte Move-Konstruktor trivial ist.
Ein trivialer Move-Konstruktor ist ein Konstruktor, der die gleiche Aktion wie der triviale Kopierkonstruktor ausführt, d.h. eine Kopie der Objekt-Repräsentation erstellt, als ob durch std::memmove. Alle mit der C-Sprache kompatiblen Datentypen sind trivial verschiebbar.
[bearbeiten] Berechtigter Move-Konstruktor
|
Ein Move-Konstruktor ist berechtigt, wenn er nicht gelöscht ist. |
(bis C++20) |
|
Ein Move-Konstruktor ist berechtigt, wenn alle folgenden Bedingungen erfüllt sind
|
(seit C++20) |
Die Trivialität von berechtigten Move-Konstruktoren bestimmt, ob die Klasse ein implizit-Lebenszeit-Typ ist und ob die Klasse ein trivial kopierbarer Typ ist.
[bearbeiten] Hinweise
Um den starken Ausnahmegarantie zu ermöglichen, sollten benutzerdefinierte Move-Konstruktoren keine Ausnahmen auslösen. Zum Beispiel verwendet std::vector std::move_if_noexcept, um zwischen Verschieben und Kopieren zu wählen, wenn Elemente neu positioniert werden müssen.
Wenn sowohl Kopier- als auch Move-Konstruktoren bereitgestellt werden und keine anderen Konstruktoren möglich sind, wählt die Überladungsauflösung den Move-Konstruktor, wenn das Argument ein rvalue desselben Typs ist (ein xvalue wie das Ergebnis von std::move oder ein prvalue wie ein namenloses temporäres Objekt(bis C++17)), und wählt den Kopierkonstruktor, wenn das Argument ein lvalue ist (benanntes Objekt oder eine Funktion/ein Operator, der eine lvalue-Referenz zurückgibt). Wenn nur der Kopierkonstruktor bereitgestellt wird, wählen alle Argumentkategorien ihn aus (solange er eine Referenz auf `const` nimmt, da rvalues an const-Referenzen gebunden werden können), was das Kopieren zum Fallback für das Verschieben macht, wenn das Verschieben nicht verfügbar ist.
[bearbeiten] Beispiel
#include <iomanip> #include <iostream> #include <string> #include <utility> struct A { std::string s; int k; A() : s("test"), k(-1) {} A(const A& o) : s(o.s), k(o.k) { std::cout << "move failed!\n"; } A(A&& o) noexcept : s(std::move(o.s)), // explicit move of a member of class type k(std::exchange(o.k, 0)) // explicit move of a member of non-class type {} }; A f(A a) { return a; } struct B : A { std::string s2; int n; // implicit move constructor B::(B&&) // calls A's move constructor // calls s2's move constructor // and makes a bitwise copy of n }; struct C : B { ~C() {} // destructor prevents implicit move constructor C::(C&&) }; struct D : B { D() {} ~D() {} // destructor would prevent implicit move constructor D::(D&&) D(D&&) = default; // forces a move constructor anyway }; int main() { std::cout << "Trying to move A\n"; A a1 = f(A()); // return by value move-constructs the target // from the function parameter std::cout << "Before move, a1.s = " << std::quoted(a1.s) << " a1.k = " << a1.k << '\n'; A a2 = std::move(a1); // move-constructs from xvalue std::cout << "After move, a1.s = " << std::quoted(a1.s) << " a1.k = " << a1.k << '\n'; std::cout << "\nTrying to move B\n"; B b1; std::cout << "Before move, b1.s = " << std::quoted(b1.s) << "\n"; B b2 = std::move(b1); // calls implicit move constructor std::cout << "After move, b1.s = " << std::quoted(b1.s) << "\n"; std::cout << "\nTrying to move C\n"; C c1; C c2 = std::move(c1); // calls copy constructor std::cout << "\nTrying to move D\n"; D d1; D d2 = std::move(d1); }
Ausgabe
Trying to move A Before move, a1.s = "test" a1.k = -1 After move, a1.s = "" a1.k = 0 Trying to move B Before move, b1.s = "test" After move, b1.s = "" Trying to move C move failed! Trying to move D
[bearbeiten] Fehlerberichte
Die folgenden Verhaltensändernden Fehlerberichte wurden rückwirkend auf zuvor veröffentlichte C++-Standards angewendet.
| DR | angewendet auf | Verhalten wie veröffentlicht | Korrigiertes Verhalten |
|---|---|---|---|
| CWG 1353 | C++11 | die Bedingungen, unter denen standardmäßig festgelegte Move-Konstruktoren als gelöscht definiert werden, berücksichtigten keine mehrdimensionalen Array-Typen |
diese Typen berücksichtigen |
| CWG 1402 | C++11 | ein standardmäßig festgelegter Move-Konstruktor, der einen nicht-trivialen Kopierkonstruktor aufrufen würde, wurde als gelöscht definiert; ein standardmäßig festgelegter Move-Konstruktor, der gelöscht ist, nahm weiterhin an der Überladungsauflösung teil |
ermöglicht den Aufruf eines solchen Kopierens Konstruktor; wurde ignoriert in der Überladungsauflösung |
| CWG 1491 | C++11 | ein standardmäßig festgelegter Move-Konstruktor einer Klasse mit einem nicht-statischen Daten- Mitglied mit rvalue-Referenztyp wurde als gelöscht definiert |
in diesem Fall nicht gelöscht |
| CWG 2094 | C++11 | ein volatiles Subobjekt machte einen standardmäßig festgelegten Move-Konstruktor nicht-trivial (CWG-Problem 496) |
Trivialität nicht betroffen |
| CWG 2595 | C++20 | ein Move-Konstruktor war nicht berechtigt, wenn es einen anderen Move-Konstruktor gab, der eingeschränkter war aber seine zugehörigen Constraints nicht erfüllte |
er kann in diesem Fall berechtigt sein |