Namensräume
Varianten
Aktionen

Kopier-Eliminierung

Von cppreference.com
< cpp‎ | Sprache
 
 
C++ Sprache
Allgemeine Themen
Kontrollfluss
Bedingte Ausführungsaussagen
if
Iterationsanweisungen (Schleifen)
for
Bereichs-for (C++11)
Sprunganweisungen
Funktionen
Funktionsdeklaration
Lambda-Funktionsausdruck
inline-Spezifizierer
Dynamische Ausnahmespezifikationen (bis C++17*)
noexcept-Spezifizierer (C++11)
Ausnahmen
Namensräume
Typen
Spezifizierer
const/volatile
decltype (C++11)
auto (C++11)
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Speicherdauer-Spezifizierer
Initialisierung
Ausdrücke
Alternative Darstellungen
Literale
Boolesch - Ganzzahl - Gleitkommazahl
Zeichen - String - nullptr (C++11)
Benutzerdefinierte (C++11)
Dienstprogramme
Attribute (C++11)
Typen
typedef-Deklaration
Typalias-Deklaration (C++11)
Umwandlungen
Speicherzuweisung
Klassen
Klassenspezifische Funktionseigenschaften
explicit (C++11)
static

Spezielle Member-Funktionen
Templates
Sonstiges
 
 

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.
  • Wenn ein Klassenobjekt target durch Kopierinitialisierung mit einem temporären Klassenobjekt obj initialisiert wird, das nicht an eine Referenz gebunden wurde, kann die Kopierinitialisierung weggelassen werden, indem obj direkt in target konstruiert wird. Diese Variante der Kopierelision ist als Unnamed Return Value Optimization (URVO) bekannt. Seit C++17 ist URVO zwingend und wird nicht mehr als eine Form der Kopierelision betrachtet; siehe unten.
(bis C++17)
  • In einem throw-Ausdruck, wenn der Operand der Name eines nicht-volatilen Objekts obj mit automatischer Speicherverwaltung ist (außer einem Funktionsparameter oder einem Handler-Parameter), das zu einem Gültigkeitsbereich gehört, der nicht den innersten umschließenden try-Block (falls vorhanden) enthält, kann die Kopierinitialisierung des Ausnahmeobjekts weggelassen werden, indem obj direkt in das Ausnahmeobjekt konstruiert wird.
  • In einem Handler kann die Kopierinitialisierung des Handler-Arguments weggelassen werden, indem der Handler-Parameter als Alias für das Ausnahmeobjekt behandelt wird, wenn die Bedeutung des Programms unverändert bleibt, mit Ausnahme der Ausführung von Konstruktoren und Destruktoren für das Handler-Argument.
(seit C++11)
  • In Coroutinen kann eine Kopie eines Coroutinenparameters weggelassen werden. In diesem Fall werden Referenzen auf diese Kopie durch Referenzen auf den entsprechenden Parameter ersetzt, wenn die Bedeutung des Programms unverändert bleibt, mit Ausnahme der Ausführung eines Konstruktors und eines Destruktors für das Kopierobjekt des Parameters.
(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

  • Die Initialisierung des zurückgegebenen Objekts in einer return-Anweisung, wenn der Operand ein Prvalue desselben Klassentyps (unter Ignorierung der cv-Qualifikation) wie der Rückgabetyp der Funktion ist
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
}
Der Destruktor des zurückgegebenen Typs muss an der Stelle der return-Anweisung zugänglich und nicht gelöscht sein, auch wenn kein T-Objekt zerstört wird.
  • Bei der Initialisierung eines Objekts, wenn der Initialisierungsausdruck ein Prvalue desselben Klassentyps (unter Ignorierung der cv-Qualifikation) wie der Variablentyp ist
T x = T(T(f())); // x is initialized by the result of f() directly; no move
Dies kann nur angewendet werden, wenn das zu initialisierende Objekt kein potenziell überlappendes Unterobjekt ist
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

[bearbeiten] Siehe auch