C++
Kopieer Elision
Zoeken…
Doel van kopie elision
Er zijn plaatsen in de standaard waar een object wordt gekopieerd of verplaatst om een object te initialiseren. Copy elision (soms retourwaarde-optimalisatie genoemd) is een optimalisatie waarbij, onder bepaalde specifieke omstandigheden, een compiler is toegestaan de kopie te vermijden of te verplaatsen, hoewel de standaard zegt dat dit moet gebeuren.
Overweeg de volgende functie:
std::string get_string()
{
return std::string("I am a string.");
}
Volgens de strikte formulering van de standaard zal deze functie een tijdelijke std::string
initialiseren, deze vervolgens kopiëren / verplaatsen naar het retourwaardeobject en vervolgens de tijdelijke vernietigen. De standaard is heel duidelijk dat dit is hoe de code wordt geïnterpreteerd.
Copy elision is een regel waarmee een C ++ compiler het maken van de tijdelijke en de daaropvolgende kopie / vernietiging kan negeren . Dat wil zeggen dat de compiler de initialisatie-expressie voor de tijdelijke kan nemen en de retourwaarde van de functie hieruit direct kan initialiseren. Dit bespaart duidelijk prestaties.
Het heeft echter twee zichtbare effecten op de gebruiker:
Het type moet de copy / move-constructor hebben die zou zijn aangeroepen. Zelfs als de compiler het kopiëren / verplaatsen opheft, moet het type nog steeds kunnen worden gekopieerd / verplaatst.
Bijwerkingen van kopieer- / verplaatsingsconstructors zijn niet gegarandeerd in omstandigheden waarin elision kan plaatsvinden. Stel je de volgende situatie voor:
struct my_type
{
my_type() = default;
my_type(const my_type &) {std::cout <<"Copying\n";}
my_type(my_type &&) {std::cout <<"Moving\n";}
};
my_type func()
{
return my_type();
}
Wat doet func
? Welnu, het zal nooit "Kopiëren" afdrukken, omdat het tijdelijke een my_type
is en my_type
een verplaatsbaar type is. Dus zal het "Verhuizen" afdrukken?
Zonder de kopieerbeslissingsregel zou dit vereist zijn om altijd "Verplaatsen" af te drukken. Maar omdat de kopieerbeslissingsregel bestaat, kan de constructor van de beweging wel of niet worden aangeroepen; het is afhankelijk van de implementatie.
En daarom kunt u niet afhankelijk zijn van de aanroep van kopieer- / verplaatsingsconstructors in contexten waar elision mogelijk is.
Omdat elision een optimalisatie is, ondersteunt uw compiler mogelijk niet in alle gevallen elision. En ongeacht of de compiler een bepaald geval verwijdert of niet, het type moet nog steeds de bewerking ondersteunen die wordt geëlimineerd. Dus als een kopieconstructie wordt weggelaten, moet het type nog steeds een kopieconstructie hebben, hoewel deze niet wordt aangeroepen.
Gegarandeerde kopie-eliminatie
Normaal is elision een optimalisatie. Terwijl vrijwel elke compiler kopie-elision in de eenvoudigste gevallen ondersteunt, legt het hebben van elision nog steeds een bijzondere last op gebruikers. Het type wiens kopie / verplaatsing wordt geëlimineerd, moet namelijk nog steeds de kopieer- / verplaatsingsbewerking hebben die is verwijderd.
Bijvoorbeeld:
std::mutex a_mutex;
std::lock_guard<std::mutex> get_lock()
{
return std::lock_guard<std::mutex>(a_mutex);
}
Dit kan handig zijn in gevallen waarin a_mutex
een mutex is die privé wordt a_mutex
door een systeem, maar een externe gebruiker wil er misschien een scoped-lock op hebben.
Dit is ook niet legaal, omdat std::lock_guard
niet kan worden gekopieerd of verplaatst. Hoewel vrijwel elke C ++ compiler de kopie / verplaatsing zal elimineren, vereist de standaard nog steeds het type om die bewerking beschikbaar te hebben.
Tot C ++ 17.
C ++ 17 verplicht elision door de betekenis van bepaalde uitdrukkingen effectief te herdefiniëren, zodat er geen kopie / verplaatsing plaatsvindt. Overweeg de bovenstaande code.
Onder pre-C ++ 17 formulering, zegt die code om een tijdelijke te maken en vervolgens de tijdelijke te gebruiken om te kopiëren / verplaatsen naar de retourwaarde, maar de tijdelijke kopie kan worden verwijderd. Onder de formulering C ++ 17 creëert dat helemaal geen tijdelijk.
In C ++ 17 genereert geen prvalue-expressie , wanneer deze wordt gebruikt om een object van hetzelfde type als de expressie te initialiseren, geen tijdelijk. De uitdrukking initialiseert dat object direct. Als u een prvalue van hetzelfde type retourneert als de retourwaarde, hoeft het type geen constructor voor kopiëren / verplaatsen te hebben. En daarom, volgens C ++ 17-regels, kan de bovenstaande code werken.
De formulering C ++ 17 werkt in gevallen waarin het type van de prvalue overeenkomt met het type dat wordt geïnitialiseerd. Dus gezien get_lock
hierboven, vereist dit ook geen kopie / verplaatsing:
std::lock_guard the_lock = get_lock();
Aangezien het resultaat van get_lock
een prvalue-expressie is die wordt gebruikt om een object van hetzelfde type te initialiseren, zal er niet worden gekopieerd of verplaatst. Die uitdrukking creëert nooit een tijdelijk; het wordt gebruikt om the_lock
direct te initialiseren. Er is geen elision omdat er geen kopie / verplaatsing is om elide te verwijderen.
De term "gegarandeerde kopie-eliminatie" is daarom iets van een verkeerde benaming, maar dat is de naam van het kenmerk zoals het wordt voorgesteld voor C ++ 17-standaardisatie . Het garandeert helemaal geen verkiezing; het elimineert de kopie / verplaatsing helemaal, waarbij C ++ opnieuw wordt gedefinieerd, zodat er nooit een kopie / verplaatsing is die moet worden verwijderd.
Deze functie werkt alleen in gevallen waarin een waarde-uitdrukking wordt gebruikt. Als zodanig gebruikt dit de gebruikelijke verkiezingsregels:
std::mutex a_mutex;
std::lock_guard<std::mutex> get_lock()
{
std::lock_guard<std::mutex> my_lock(a_mutex);
//Do stuff
return my_lock;
}
Hoewel dit een geldig geval is voor kopie- eliminatie, elimineren C ++ 17-regels in dit geval de kopie / verplaatsing niet. Als zodanig moet het type nog steeds een kopieer / verplaats-constructor hebben om de retourwaarde te initialiseren. En aangezien lock_guard
dat niet doet, is dit nog steeds een compileerfout. Implementaties mogen kopieën weigeren bij het passeren of retourneren van een object van triviaal kopieerbaar type. Dit is om het verplaatsen van dergelijke objecten in registers mogelijk te maken, die sommige ABI's in hun roepconventies kunnen opleggen.
struct trivially_copyable {
int a;
};
void foo (trivially_copyable a) {}
foo(trivially_copyable{}); //copy elision not mandated
Retourwaarde elision
Als u een prvalue-expressie retourneert vanuit een functie en de prvalue-expressie van hetzelfde type is als het retourtype van de functie, kan de kopie uit de tijdelijke prvalue worden verwijderd:
std::string func()
{
return std::string("foo");
}
Vrijwel alle compilers zullen in dit geval de tijdelijke constructie uit de weg gaan.
Parameter elision
Wanneer u een argument doorgeeft aan een functie en het argument een prvalue-expressie is van het parametertype van de functie, en dit type geen verwijzing is, kan de constructie van de prvalue worden weggelaten.
void func(std::string str) { ... }
func(std::string("foo"));
Dit zegt dat u een tijdelijke string
moet maken en deze vervolgens naar de functieparameter str
moet verplaatsen. Met Copy elision kan deze expressie het object rechtstreeks in str
, in plaats van een tijdelijke + verplaatsing te gebruiken.
Dit is een nuttige optimalisatie voor gevallen waarin een constructor explicit
wordt verklaard. We hadden het bovenstaande bijvoorbeeld kunnen schrijven als func("foo")
, maar alleen omdat string
een impliciete constructor heeft die converteert van een const char*
naar een string
. Als die constructor explicit
, zouden we genoodzaakt zijn een tijdelijk te gebruiken om de explicit
constructor aan te roepen. Copy elision voorkomt dat we onnodig moeten kopiëren / verplaatsen.
Genoemde retourwaarde-elision
Als u een lvalue-uitdrukking van een functie retourneert en deze waarde:
- staat voor een automatische variabele lokaal voor die functie, die na de
return
wordt vernietigd - de automatische variabele is geen functieparameter
- en het type van de variabele is hetzelfde type als het retourtype van de functie
Als dit allemaal het geval is, kan de kopie / verplaatsing van de waarde worden verwijderd:
std::string func()
{
std::string str("foo");
//Do stuff
return str;
}
Complexere cases komen in aanmerking voor verkiezing, maar hoe complexer de case, hoe minder waarschijnlijk de compiler zal zijn om deze daadwerkelijk te elimineren:
std::string func()
{
std::string ret("foo");
if(some_condition)
{
return "bar";
}
return ret;
}
De compiler kan nog steeds uitwijken voor ret
, maar de kans dat ze dat doen, neemt af.
Zoals eerder opgemerkt, is elisie niet toegestaan waardeparameters.
std::string func(std::string str)
{
str.assign("foo");
//Do stuff
return str; //No elision possible
}
Initialisatie-kopie kopiëren
Als u een prvalue-expressie gebruikt om te kopiëren, initialiseert u een variabele en die variabele heeft hetzelfde type als de prvalue-expressie, dan kan het kopiëren worden verwijderd.
std::string str = std::string("foo");
Initialisatie kopiëren kopieert dit effectief naar std::string str("foo");
(er zijn kleine verschillen).
Dit werkt ook met retourwaarden:
std::string func()
{
return std::string("foo");
}
std::string str = func();
Zonder copy elision zou dit 2 aanroepen van std::string
's constructor veroorzaken. Copy elision staat dit toe om de constructor 1 of nul keer aan te roepen, en de meeste compilers zullen voor het laatste kiezen.