Wat is kopieerelisie? Wat is (genoemd) rendementsoptimalisatie? Wat houden ze in?
In welke situaties kunnen ze voorkomen? Wat zijn beperkingen?
- Als je naar deze vraag werd verwezen, ben je waarschijnlijk op zoek naar de introductie.
- Zie de standaardreferentievoor een technisch overzicht.
- Bekijk veelvoorkomende gevallenhier.
Antwoord 1, autoriteit 100%
Inleiding
Voor een technisch overzicht – ga naar dit antwoord.
Voor veelvoorkomende gevallen waarin kopieerelisie optreedt: ga door naar dit antwoord.
Kopieerelisie is een optimalisatie die door de meeste compilers wordt geïmplementeerd om in bepaalde situaties extra (potentieel dure) kopieën te voorkomen. Het maakt het retourneren op waarde of pass-by-waarde in de praktijk mogelijk (beperkingen zijn van toepassing).
Het is de enige vorm van optimalisatie die (ha!) de as-if-regel elimineert – kopieerelisie kan worden toegepast, zelfs als het kopiëren/verplaatsen van het object bijwerkingen heeft.
Het volgende voorbeeld uit 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();
}
Afhankelijk van de compiler & instellingen, zijn de volgende uitgangen allemaal geldig:
Hallo wereld!
Er is een kopie gemaakt.
Er is een kopie gemaakt.
Hallo wereld!
Er is een kopie gemaakt.
Hallo wereld!
Dit betekent ook dat er minder objecten kunnen worden gemaakt, dus je kunt er ook niet op vertrouwen dat een bepaald aantal destructors wordt aangeroepen. Je mag geen kritische logica hebben in copy/move-constructors of destructors, omdat je er niet op kunt vertrouwen dat ze worden aangeroepen.
Als een aanroep van een kopieer- of verplaatsingsconstructor wordt weggelaten, moet die constructor nog steeds bestaan en toegankelijk zijn. Dit zorgt ervoor dat kopieerelisie niet toestaat dat objecten worden gekopieerd die normaal niet kunnen worden gekopieerd, b.v. omdat ze een privé of verwijderde copy/move-constructor hebben.
C++17: vanaf C++17 is Copy Elision gegarandeerd wanneer een object direct wordt geretourneerd:
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
}
Antwoord 2, autoriteit 36%
Standaard referentie
Voor een minder technisch beeld & introductie – ga door naar dit antwoord.
Voor veelvoorkomende gevallen waarin kopieerelisie optreedt: ga door naar dit antwoord.
Kopieer elisiewordt gedefinieerd in de standaard in:
12.8 Klassenobjecten kopiëren en verplaatsen [class.copy]
als
31) Wanneer aan bepaalde criteria wordt voldaan, mag een implementatie de kopieer-/verplaatsingsconstructie van een klasse weglaten
object, zelfs als de copy/move-constructor en/of destructor voor het object neveneffecten hebben. In dergelijke gevallen,
de implementatie behandelt de bron en het doel van de weggelaten kopieer-/verplaatsingsbewerking als gewoon twee verschillende
manieren om naar hetzelfde object te verwijzen, en de vernietiging van dat object vindt plaats op een later tijdstip
wanneer de twee objecten zouden zijn vernietigd zonder de optimalisatie.123Deze elisie van kopiëren/verplaatsen
bewerkingen, genaamd copy elision, is toegestaan in de volgende omstandigheden (die kunnen worden gecombineerd tot:
elimineer meerdere kopieën):— in een return-instructie in een functie met een class-retourtype, wanneer de expressie de naam is van a
niet-vluchtig automatisch object (anders dan een functie- of catch-clausuleparameter) met dezelfde cvunqualified
type als het retourtype van de functie, kan de bewerking kopiëren/verplaatsen worden weggelaten door te construeren
het automatische object direct in de retourwaarde van de functie— in een throw-expressie, wanneer de operand de naam is van een niet-vluchtig automatisch object (anders dan een
functie of catch-clause parameter) waarvan de reikwijdte niet verder reikt dan het einde van de binnenste
try-block insluiten (als die er is), de kopieer-/verplaatsingsbewerking van de operand naar de uitzondering
object (15.1) kan worden weggelaten door het automatische object rechtstreeks in het uitzonderingsobject te construeren— wanneer een tijdelijk klasseobject dat niet aan een referentie (12.2) is gebonden, zou worden gekopieerd/verplaatst
naar een klasseobject met hetzelfde cv-unqualified type, kan de bewerking kopiëren/verplaatsen worden weggelaten door:
het tijdelijke object rechtstreeks construeren in het doel van de weggelaten kopiëren/verplaatsen— wanneer de uitzonderingsverklaring van een uitzonderingsbehandelaar (clausule 15) een object van hetzelfde type declareert
(behalve voor cv-kwalificatie) als uitzonderingsobject (15.1), kan de bewerking kopiëren/verplaatsen worden weggelaten
door de uitzonderingsverklaring te behandelen als een alias voor het uitzonderingsobject als de betekenis van het programma
zal ongewijzigd blijven, behalve voor de uitvoering van constructors en destructors voor het object gedeclareerd door
de uitzonderingsverklaring.123) Omdat er maar één object wordt vernietigd in plaats van twee, en één copy/move-constructor niet wordt uitgevoerd, is er nog steeds één
object vernietigd voor elke gebouwde.
Het gegeven voorbeeld is:
class Thing {
public:
Thing();
~Thing();
Thing(const Thing&);
};
Thing f() {
Thing t;
return t;
}
Thing t2 = f();
en uitgelegd:
Hier kunnen de criteria voor elisie worden gecombineerd om twee aanroepen naar de kopieerconstructor van klasse
Thing
te elimineren:
het kopiëren van het lokale automatische objectt
naar het tijdelijke object voor de retourwaarde van functief()
en het kopiëren van dat tijdelijke object naar objectt2
. In feite is de constructie van het lokale objectt
kan worden gezien als het direct initialiseren van het globale objectt2
, en de vernietiging van dat object zal plaatsvinden op het programma
Uitgang. Het toevoegen van een verplaatsingsconstructor aan Thing heeft hetzelfde effect, maar het is de verplaatsingsconstructie van de
tijdelijk object naart2
dat wordt weggelaten.
Antwoord 3, autoriteit 35%
Veelvoorkomende vormen van kopieerelisie
Voor een technisch overzicht – ga naar dit antwoord.
Voor een minder technisch beeld & introductie – ga door naar dit antwoord.
(Benoemd) Optimalisatie van de retourwaarde is een veel voorkomende vorm van kopieerelisie. Het verwijst naar de situatie waarin een object dat wordt geretourneerd door waarde van een methode, waarvan de kopie wordt weggelaten. Het voorbeeld dat in de norm wordt uiteengezet, illustreert optimalisatie van de retourwaarde met een naam, aangezien het object een naam heeft.
class Thing {
public:
Thing();
~Thing();
Thing(const Thing&);
};
Thing f() {
Thing t;
return t;
}
Thing t2 = f();
Regelmatige optimalisatie van de retourwaardevindt plaats wanneer een tijdelijke wordt geretourneerd:
class Thing {
public:
Thing();
~Thing();
Thing(const Thing&);
};
Thing f() {
return Thing();
}
Thing t2 = f();
Andere veelvoorkomende plaatsen waar kopieerelisie plaatsvindt, is wanneer een tijdelijke waarde wordt doorgegeven:
class Thing {
public:
Thing();
~Thing();
Thing(const Thing&);
};
void foo(Thing t);
foo(Thing());
of wanneer een uitzondering wordt gegooid en gevangen door waarde:
struct Thing{
Thing();
Thing(const Thing&);
};
void foo() {
Thing c;
throw c;
}
int main() {
try {
foo();
}
catch(Thing c) {
}
}
Veelvoorkomende beperkingen van kopieerelisie zijn :
- meerdere retourpunten
- voorwaardelijke initialisatie
De meeste commerciële compilers ondersteunen copy elision & (N)RVO (afhankelijk van optimalisatie-instellingen).
Antwoord 4, autoriteit 22%
Copy elision is een compiler-optimalisatietechniek die onnodig kopiëren/verplaatsen van objecten elimineert.
In de volgende omstandigheden is het een compiler toegestaan om kopieer-/verplaatsingsbewerkingen weg te laten en daarom de bijbehorende constructor niet aan te roepen:
- NRVO (Named Return Value Optimization): als een functie een klassetype op waarde retourneert en de expressie van de return-instructie de naam is van een niet-vluchtig object met automatische opslagduur (wat niet een functieparameter), dan kan het kopiëren/verplaatsen dat zou worden uitgevoerd door een niet-optimaliserende compiler worden weggelaten. Als dat het geval is, wordt de geretourneerde waarde rechtstreeks in de opslag geconstrueerd waarnaar de geretourneerde waarde van de functie anders zou worden verplaatst of gekopieerd.
- RVO (Return Value Optimization): als de functie een naamloos tijdelijk object retourneert dat door een naïeve compiler naar de bestemming zou worden verplaatst of gekopieerd, kan het kopiëren of verplaatsen worden weggelaten volgens 1.
#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
Zelfs wanneer kopieerelisie plaatsvindt en de copy-/move-constructor niet wordt aangeroepen, moet deze aanwezig en toegankelijk zijn (alsof er helemaal geen optimalisatie heeft plaatsgevonden), anders is het programma slecht gevormd.
U dient dergelijke kopieerelisie alleen toe te staan op plaatsen waar dit het waarneembare gedrag van uw software niet beïnvloedt. Kopieerelisie is de enige vorm van optimalisatie die waarneembare neveneffecten heeft (d.w.z. elimineren). Voorbeeld:
#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
GCC biedt de optie -fno-elide-constructors
om kopieerelisie uit te schakelen.
Gebruik -fno-elide-constructors
als je mogelijke kopieerelisie wilt vermijden.
Nu bieden bijna alle compilers kopieerelisie wanneer optimalisatie is ingeschakeld (en als er geen andere optie is ingesteld om het uit te schakelen).
Conclusie
Bij elke kopie-elisie wordt één constructie en één overeenkomende vernietiging van de kopie weggelaten, waardoor CPU-tijd wordt bespaard, en er wordt geen object gemaakt, waardoor ruimte op het stapelframe wordt bespaard.
Antwoord 5
Hier geef ik nog een voorbeeld van kopieerelisie die ik vandaag blijkbaar ben tegengekomen.
# 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";
}
}
Met als resultaat:
In Obj()
In Obj()
assignment:
In Obj(const Obj & org)
Obj_instance2.var1:3