Kopier-Eliminierung
Wenn bestimmte Kriterien erfüllt sind, kann die Erstellung eines Klassenobjekts aus einem Quellobjekt desselben Typs (unter Ignorierung der cv-Qualifikation) weggelassen werden, selbst wenn der ausgewählte Konstruktor und/oder der Destruktor für das Objekt Nebeneffekte haben. Dieses Weglassen der Objekterstellung wird als Kopierelision bezeichnet.
Inhalt |
[bearbeiten] Erklärung
Kopierelision ist unter den folgenden Umständen zulässig (die kombiniert werden können, um mehrere Kopien zu eliminieren)
- In einer return-Anweisung in einer Funktion mit einem Klassentyp als Rückgabewert, wenn der Operand der Name eines nicht-volatilen Objekts obj mit automatischer Speicherverwaltung ist (außer einem Funktionsparameter oder einem Handler-Parameter), kann die Kopierinitialisierung des Ergebnisobjekts weggelassen werden, indem obj direkt in das Ergebnisobjekt des Funktionsaufrufs konstruiert wird. Diese Variante der Kopierelision ist als Named Return Value Optimization (NRVO) bekannt.
|
(bis C++17) |
|
(seit C++11) |
|
(seit C++20) |
Wenn Kopierelision auftritt, behandelt die Implementierung die Quelle und das Ziel der weggelassenen Initialisierung einfach als zwei verschiedene Möglichkeiten, auf dasselbe Objekt zu verweisen.
|
Die Zerstörung erfolgt zum späteren Zeitpunkt, an dem die beiden Objekte ohne die Optimierung zerstört worden wären. |
(bis C++11) |
|
Wenn der erste Parameter des ausgewählten Konstruktors eine rvalue-Referenz auf den Typ des Objekts ist, erfolgt die Zerstörung dieses Objekts zum Zeitpunkt, an dem das Ziel zerstört worden wäre. Andernfalls erfolgt die Zerstörung zum späteren Zeitpunkt, an dem die beiden Objekte ohne die Optimierung zerstört worden wären. |
(seit C++11) |
Prvalue-Semantik ("garantierte Kopierelision")Seit C++17 wird ein Prvalue erst bei Bedarf materialisiert und dann direkt in den Speicher seines endgültigen Ziels konstruiert. Dies bedeutet manchmal, dass selbst wenn die Sprachsyntax eine Kopie/einen Move suggeriert (z. B. bei Kopierinitialisierung), keine Kopie/kein Move durchgeführt wird – was bedeutet, dass der Typ keinen zugänglichen Kopier-/Move-Konstruktor haben muss. Beispiele hierfür sind
T f() { return U(); // constructs a temporary of type U, // then initializes the returned T from the temporary } T g() { return T(); // constructs the returned T directly; no move }
T x = T(T(f())); // x is initialized by the result of f() directly; no move
struct C { /* ... */ }; C f(); struct D; D g(); struct D : C { D() : C(f()) {} // no elision when initializing a base class subobject D(int) : D(g()) {} // no elision because the D object being initialized might // be a base-class subobject of some other class }; Hinweis: Diese Regel spezifiziert keine Optimierung, und der Standard beschreibt sie nicht formell als "Kopierelision" (da nichts weggelassen wird). Stattdessen ist die C++17-Kernsprachen-Spezifikation von Prvalues und Temporaries grundlegend anders als die früherer C++-Revisionen: Es gibt kein temporäres Objekt mehr, von dem kopiert/moved werden könnte. Eine andere Möglichkeit, die C++17-Mechanik zu beschreiben, ist "unmaterialized value passing" oder "deferred temporary materialization": Prvalues werden zurückgegeben und verwendet, ohne jemals ein temporäres Objekt zu materialisieren. |
(seit C++17) |
[bearbeiten] Anmerkungen
Kopierelision ist die einzige zulässige Form der Optimierung(bis C++14) eine von zwei zulässigen Formen der Optimierung, neben Allokationselision und -erweiterung,(seit C++14) die beobachtbare Nebeneffekte ändern kann. Da einige Compiler die Kopierelision nicht in jeder erlaubten Situation durchführen (z. B. im Debug-Modus), sind Programme, die auf die Nebeneffekte von Kopier-/Move-Konstruktoren und Destruktoren angewiesen sind, nicht portabel.
|
In einer return-Anweisung oder einem throw-Ausdruck, wenn der Compiler keine Kopierelision durchführen kann, die Bedingungen für die Kopierelision aber erfüllt sind oder wären, außer dass die Quelle ein Funktionsparameter ist, wird der Compiler versuchen, den Move-Konstruktor zu verwenden, auch wenn der Quelloperand durch einen lvalue bezeichnet wird(bis C++23) wird der Quelloperand als rvalue behandelt(seit C++23); siehe return-Anweisung für Details. In Konstantausdrücken und Konstanteninitialisierung wird niemals eine Kopierelision durchgeführt. struct A { void* p; constexpr A() : p(this) {} A(const A&); // Disable trivial copyability }; constexpr A a; // OK: a.p points to a constexpr A f() { A x; return x; } constexpr A b = f(); // error: b.p would be dangling and point to the x inside f constexpr A c = A(); // (until C++17) error: c.p would be dangling and point to a temporary // (since C++17) OK: c.p points to c; no temporary is involved |
(seit C++11) |
| Feature-Testmakro | Wert | Std | Feature |
|---|---|---|---|
__cpp_guaranteed_copy_elision |
201606L |
(C++17) | Garantierte Kopierelision durch vereinfachte Wertkategorien |
[bearbeiten] Beispiel
#include <iostream> struct Noisy { Noisy() { std::cout << "constructed at " << this << '\n'; } Noisy(const Noisy&) { std::cout << "copy-constructed\n"; } Noisy(Noisy&&) { std::cout << "move-constructed\n"; } ~Noisy() { std::cout << "destructed at " << this << '\n'; } }; Noisy f() { Noisy v = Noisy(); // (until C++17) copy elision initializing v from a temporary; // the move constructor may be called // (since C++17) "guaranteed copy elision" return v; // copy elision ("NRVO") from v to the result object; // the move constructor may be called } void g(Noisy arg) { std::cout << "&arg = " << &arg << '\n'; } int main() { Noisy v = f(); // (until C++17) copy elision initializing v from the result of f() // (since C++17) "guaranteed copy elision" std::cout << "&v = " << &v << '\n'; g(f()); // (until C++17) copy elision initializing arg from the result of f() // (since C++17) "guaranteed copy elision" }
Mögliche Ausgabe
constructed at 0x7fffd635fd4e &v = 0x7fffd635fd4e constructed at 0x7fffd635fd4f &arg = 0x7fffd635fd4f destructed at 0x7fffd635fd4f destructed at 0x7fffd635fd4e
[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 1967 | C++11 | wenn Kopierelision unter Verwendung eines Move-Konstruktors durchgeführt wird, die Lebensdauer des moved-from-Objekts wurde noch betrachtet |
nicht betrachtet |
| CWG 2426 | C++17 | Destruktor war beim Zurückgeben eines Prvalues nicht erforderlich | Destruktor wird potenziell aufgerufen |
| CWG 2930 | C++98 | nur Kopier-/Move-Operationen konnten elidiert werden, aber ein Nicht-Kopier-/Move-Konstruktor kann durch Kopierinitialisierung ausgewählt werden |
elidiert jede Objektkonstruktion von verwandten Kopierinitialisierungen |