538 Stimmen

Was sind Kopierauslassung und Rückgabewertoptimierung?

Was ist die Elision von Kopien? Was ist eine (benannte) Rückgabewertoptimierung? Was bedeuten sie?

In welchen Situationen können sie auftreten? Was sind die Grenzen?

374voto

Luchian Grigore Punkte 244505

Einführung

Für einen technischen Überblick - Überspringen Sie diese Antwort .

Für häufige Fälle, in denen die Kopie weggelassen wird - Überspringen Sie diese Antwort .

Copy Elision ist eine Optimierung, die von den meisten Compilern implementiert wird, um in bestimmten Situationen zusätzliche (potenziell teure) Kopien zu vermeiden. Sie macht die Rückgabe nach Wert oder die Weitergabe nach Wert in der Praxis möglich (es gelten Einschränkungen).

Es ist die einzige Form der Optimierung, die die Als-ob-Regel ausschließt (ha!). Kopierelimination kann auch dann angewandt werden, wenn das Kopieren/Verschieben des Objekts Nebeneffekte hat .

Das folgende Beispiel stammt aus Wikipedia :

struct C {
  C() {}
  C(const C&) { std::cout << "A copy was made.\n"; }
};

C f() {
  return C();
}

int main() {
  std::cout << "Hello World!\n";
  C obj = f();
}

Je nach Compiler und Einstellungen werden die folgenden Ergebnisse ausgegeben sind alle gültig :

Hallo Welt!
Eine Kopie wurde angefertigt.
Eine Kopie wurde angefertigt.


Hallo Welt!
Eine Kopie wurde angefertigt.


Hallo Welt!

Dies bedeutet auch, dass weniger Objekte erstellt werden können, so dass man sich auch nicht darauf verlassen kann, dass eine bestimmte Anzahl von Destruktoren aufgerufen wird. Sie sollten keine kritische Logik innerhalb von Kopier-/Verschiebe-Konstruktoren oder -Destruktoren haben, da Sie sich nicht darauf verlassen können, dass diese aufgerufen werden.

Wenn ein Aufruf eines Kopier- oder Verschiebekonstruktors elidiert wird, muss dieser Konstruktor noch existieren und zugänglich sein. Damit wird sichergestellt, dass das Kopieren von Objekten, die normalerweise nicht kopierbar sind, z. B. weil sie einen privaten oder gelöschten Kopier-/Verschiebekonstruktor haben, nicht möglich ist.

C++17 : Ab C++17 ist Copy Elision garantiert, wenn ein Objekt direkt zurückgegeben wird:

struct C {
  C() {}
  C(const C&) { std::cout << "A copy was made.\n"; }
};

C f() {
  return C(); //Definitely performs copy elision
}
C g() {
    C c;
    return c; //Maybe performs copy elision
}

int main() {
  std::cout << "Hello World!\n";
  C obj = f(); //Copy constructor isn't called
}

134voto

Luchian Grigore Punkte 244505

Häufige Formen der Elision von Kopien

Für einen technischen Überblick - Überspringen Sie diese Antwort .

Für eine weniger technische Ansicht & Einführung - Überspringen Sie diese Antwort .

Die (benannte) Rückgabewertoptimierung ist eine gängige Form der Kopierelimination. Sie bezieht sich auf die Situation, dass die Kopie eines Objekts, das als Wert von einer Methode zurückgegeben wird, weggelassen wird. Das in der Norm enthaltene Beispiel veranschaulicht Optimierung des benannten Rückgabewerts da das Objekt benannt ist.

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();

Regelmäßig Ertragswertoptimierung tritt auf, wenn ein Provisorium zurückgegeben wird:

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  return Thing();
}
Thing t2 = f();

Eine weitere häufige Stelle, an der eine Kopie weggelassen wird, ist, wenn ein Objekt konstruiert aus einem temporären :

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
void foo(Thing t);

Thing t2 = Thing();
Thing t3 = Thing(Thing()); // two rounds of elision
foo(Thing()); // parameter constructed from temporary

oder wenn ein wird eine Ausnahme ausgelöst und durch den Wert :

struct Thing{
  Thing();
  Thing(const Thing&);
};

void foo() {
  Thing c;
  throw c;
}

int main() {
  try {
    foo();
  }
  catch(Thing c) {  
  }             
}

Häufige Einschränkungen bei der Elision von Kopien sind:

  • mehrere Rückgabestellen
  • bedingte Initialisierung

Die meisten kommerziellen Compiler unterstützen Copy Elision und (N)RVO (je nach Optimierungseinstellungen). C++17 macht viele der oben genannten Klassen von Copy Elision obligatorisch.

125voto

Luchian Grigore Punkte 244505

Standard-Referenz

Für eine weniger technische Ansicht & Einführung - Überspringen Sie diese Antwort .

Für häufige Fälle, in denen die Kopie weggelassen wird - Überspringen Sie diese Antwort .

Auslassung kopieren ist in der Norm definiert in:

12.8 Kopieren und Verschieben von Klassenobjekten [class.copy]

als

31) Wenn bestimmte Kriterien erfüllt sind, darf eine Implementierung die Copy/Move-Konstruktion einer Klasse auslassen Objekts weglassen, auch wenn der Konstruktor und/oder der Destruktor für das Objekt Seiteneffekte haben. In solchen Fällen, behandelt die Implementierung die Quelle und das Ziel der ausgelassenen copy/move o als zwei verschiedene Arten, auf dasselbe Objekt zu verweisen, und die Zerstörung dieses Objekts erfolgt zu dem späteren der beiden Zeitpunkte wenn die beiden Objekte ohne die Optimierung zerstört worden wären. 123 Diese Ausklammerung von Kopier-/Verschiebe Operationen, genannt Kopierelimination ist unter den folgenden Umständen zulässig Mehrfachkopien zu eliminieren):

- in einer Rückgabeanweisung in einer Funktion mit einem Klassenrückgabetyp, wenn die exp nichtflüchtigen automatischen Objekts (außer einem Funktions- oder Catch-Clause-Parameter) mit demselben cvunqualified Typs wie der Rückgabetyp der Funktion ist, kann der Kopier-/Verschiebevorgang entfallen, indem die das automatische Objekt direkt in den Rückgabewert der Funktion

- in einem throw-Ausdruck, wenn der Operand der Name eines nichtflüchtigen automatischen Objekts ist (außer einem Funktion oder eines Catch-Klausel-Parameters), dessen Geltungsbereich nicht über das Ende des innersten einschließenden Try-Blocks (falls es einen gibt), kann die Kopier-/Verschiebeoperation vom Operanden zum Ausnahme Objekt (15.1) kann entfallen, indem das automatische Objekt direkt in das Ausnahmeobjekt konstruiert wird

- wenn ein temporäres Klassenobjekt, das nicht an eine Referenz gebunden ist (12.2), kopiert/verschoben würde auf ein Klassenobjekt mit demselben cv-unqualifizierten Typ kopiert/verschoben werden soll, kann der Kopier-/Verschiebevorgang entfallen, indem das temporäre Objekt direkt in das Ziel des ausgelassenen Kopier-/Verschiebevorgangs konstruiert wird

- wenn in der Ausnahmeerklärung eines Ausnahmebehandlungsprogramms (Klausel 15) ein Objekt o erklärt wird (außer cv-qualification) wie das Ausnahmeobjekt (15.1) deklariert, kann der Kopier-/Verschiebevorgang entfallen indem die Ausnahme-Deklaration als Alias für das Ausnahme-Objekt behandelt wird, wenn die Bedeutung des Programms unverändert bleibt, mit Ausnahme der Ausführung von Konstruktoren und Destruktoren für das durch die Ausnahmeerklärung deklarierte Objekt der Ausnahme-Erklärung deklariert wurde.

123) Da nur ein Objekt anstelle von zwei zerstört wird und ein Kopier-/Verschiebekonstruktor nicht ausgeführt wird, gibt es immer noch ein Objekt für jedes konstruierte Objekt zerstört.

Das angegebene Beispiel ist:

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();

und erklärte:

Hier können die Kriterien für die Eliminierung kombiniert werden, um zwei Aufrufe des Kopierkonstruktors der Klasse Thing : das Kopieren des lokalen automatischen Objekts t in das temporäre Objekt für den Rückgabewert der Funktion f() und das Kopieren dieses temporären Objekts in das Objekt t2 . Die Konstruktion des lokalen Objekts ist effektiv t kann als direkte Initialisierung des globalen Objekts angesehen werden t2 und die Zerstörung dieses Objekts wird erfolgen beenden. Das Hinzufügen eines move-Konstruktors zu Thing hat den gleichen Effekt, aber es ist die move-Konstruktion aus der temporären Objekt nach t2 das weggelassen wird.

78voto

Ajay yadav Punkte 3517

Copy Elision ist eine Compiler-Optimierungstechnik, die das unnötige Kopieren/Verschieben von Objekten verhindert.

Unter den folgenden Umständen darf ein Compiler Kopier-/Verschiebeoperationen auslassen und somit den zugehörigen Konstruktor nicht aufrufen:

  1. NRVO (Optimierung der benannten Rückgabewerte) : Wenn eine Funktion einen Klassentyp als Wert zurückgibt und der Ausdruck der Rückgabeanweisung der Name eines nichtflüchtigen Objekts mit automatischer Speicherdauer ist (das kein Funktionsparameter ist), dann kann das Kopieren/Verschieben, das ein nicht-optimierender Compiler durchführen würde, weggelassen werden. In diesem Fall wird der Rückgabewert direkt in dem Speicher konstruiert, in den der Rückgabewert der Funktion sonst verschoben oder kopiert werden würde.
  2. RVO (Rückgabewert-Optimierung) : Wenn die Funktion ein namenloses temporäres Objekt zurückgibt, das von einem naiven Compiler in den Zielort verschoben oder kopiert werden würde, kann das Kopieren oder Verschieben gemäß 1. weggelassen werden.

    include <iostream>

    using namespace std;

    class ABC
    {
    public:
    const char a;
    ABC()
    { cout<<"Constructor"<<endl; }
    ABC(const char
    ptr)
    { cout<<"Constructor"<<endl; }
    ABC(ABC &obj)
    { cout<<"copy constructor"<<endl;}
    ABC(ABC&& obj)
    { cout<<"Move constructor"<<endl; }
    ~ABC()
    { cout<<"Destructor"<<endl; }
    };

    ABC fun123()
    { ABC obj; return obj; }

    ABC xyz123()
    { return ABC(); }

    int main()
    {
    ABC abc;
    ABC obj1(fun123()); //NRVO
    ABC obj2(xyz123()); //RVO, not NRVO ABC xyz = "Stack Overflow";//RVO
    return 0;
    }

    Output without -fno-elide-constructors
    root@ajay-PC:/home/ajay/c++# ./a.out
    Constructor
    Constructor
    Constructor
    Constructor
    Destructor
    Destructor
    Destructor
    Destructor

    Output with -fno-elide-constructors
    root@ajay-PC:/home/ajay/c++# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors
    root@ajay-PC:/home/ajay/c++# ./a.out
    Constructor
    Constructor
    Move constructor
    Destructor
    Move constructor
    Destructor
    Constructor
    Move constructor
    Destructor
    Move constructor
    Destructor
    Constructor
    Move constructor
    Destructor
    Destructor
    Destructor
    Destructor
    Destructor

Selbst wenn der Kopier-/Verschiebungs-Konstruktor nicht aufgerufen wird, muss er vorhanden und zugänglich sein (so als ob keine Optimierung stattgefunden hätte), sonst ist das Programm schlecht geformt.

Sie sollten solche Kopierauslassungen nur dort zulassen, wo sie das beobachtbare Verhalten Ihrer Software nicht beeinflussen. Das Eliminieren von Kopien ist die einzige Form der Optimierung, die beobachtbare Nebeneffekte haben (d.h. elidieren) darf. Beispiel:

#include <iostream>     
int n = 0;    
class ABC     
{  public:  
 ABC(int) {}    
 ABC(const ABC& a) { ++n; } // the copy constructor has a visible side effect    
};                     // it modifies an object with static storage duration    

int main()   
{  
  ABC c1(21); // direct-initialization, calls C::C(42)  
  ABC c2 = ABC(21); // copy-initialization, calls C::C( C(42) )  

  std::cout << n << std::endl; // prints 0 if the copy was elided, 1 otherwise
  return 0;  
}

Output without -fno-elide-constructors  
root@ajay-PC:/home/ayadav# g++ -std=c++11 copy_elision.cpp  
root@ajay-PC:/home/ayadav# ./a.out   
0

Output with -fno-elide-constructors  
root@ajay-PC:/home/ayadav# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors  
root@ajay-PC:/home/ayadav# ./a.out   
1

Der GCC bietet die -fno-elide-constructors um die Kopierauslese zu deaktivieren. Wenn Sie die Elision von Kopien vermeiden wollen, verwenden Sie -fno-elide-constructors .

Jetzt bieten fast alle Compiler Kopierauslese an, wenn die Optimierung aktiviert ist (und wenn keine andere Option eingestellt ist, um sie zu deaktivieren).

Schlussfolgerung

Bei jeder Eliminierung einer Kopie entfallen eine Erstellung und eine entsprechende Zerstörung der Kopie, wodurch CPU-Zeit gespart wird, und ein Objekt wird nicht erstellt, wodurch Platz auf dem Stapelrahmen gespart wird.

-1voto

K.Karamazen Punkte 158

Hier gebe ich ein weiteres Beispiel für die Auslassung von Kopien, das mir heute offensichtlich begegnet ist.

# include <iostream>

class Obj {
public:
  int var1;
  Obj(){
    std::cout<<"In   Obj()"<<"\n";
    var1 =2;
  };
  Obj(const Obj & org){
    std::cout<<"In   Obj(const Obj & org)"<<"\n";
    var1=org.var1+1;
  };
};

int  main(){

  {
    /*const*/ Obj Obj_instance1;  //const doesn't change anything
    Obj Obj_instance2;
    std::cout<<"assignment:"<<"\n";
    Obj_instance2=Obj(Obj(Obj(Obj(Obj_instance1))))   ;
    // in fact expected: 6, but got 3, because of 'copy elision'
    std::cout<<"Obj_instance2.var1:"<<Obj_instance2.var1<<"\n";
  }

}

Mit dem Ergebnis:

In   Obj()
In   Obj()
assignment:
In   Obj(const Obj & org)
Obj_instance2.var1:3

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X