Kopiera elision
I C++ datorprogrammering hänvisar copy elision till en kompilatoroptimeringsteknik som eliminerar onödig kopiering av objekt .
Språkstandarden C++ tillåter i allmänhet implementeringar att utföra vilken optimering som helst, förutsatt att det resulterande programmets observerbara beteende är detsamma som om , dvs. låtsas, att programmet kördes exakt som standarden kräver. Utöver det beskriver standarden också ett fåtal situationer där kopiering kan elimineras även om detta skulle förändra programmets beteende, den vanligaste är returvärdeoptimering (se nedan ). En annan allmänt implementerad optimering, som beskrivs i C++-standarden , är när ett temporärt objekt av klasstyp kopieras till ett objekt av samma typ. Som ett resultat kopieringsinitiering vanligtvis likvärdig med direktinitiering när det gäller prestanda, men inte i semantik; kopieringsinitiering kräver fortfarande en tillgänglig kopieringskonstruktor . Optimeringen kan inte tillämpas på ett temporärt objekt som har bundits till en referens.
Exempel
0
#inkludera <iostream> int n = ; struct C { explicit C ( int ) {} C ( const C & ) { ++ n ; } // kopieringskonstruktorn har en synlig bieffekt }; // det modifierar ett objekt med statisk lagringstid int main () { C c1 ( 42 ); // direktinitiering, anropar C::C(int) C c2 = C ( 42 ); // copy-initialization, anropar C::C(const C&) std :: cout << n << std :: endl ; // skriver ut 0 om kopian har raderats, 1 annars }
Enligt standarden kan en liknande optimering tillämpas på objekt som kastas och fångas , men det är oklart om optimeringen gäller både kopian från det kastade objektet till undantagsobjektet och kopian från undantagsobjektet till objektet som deklareras i undantagsförklaringen av fångstklausulen . _ Det är också oklart om denna optimering endast gäller för tillfälliga objekt eller även namngivna objekt. Med tanke på följande källkod:
#include <iostream> struct C { C () = default ; C ( const C & ) { std :: cout << "Hej världen! \n " ; } }; void f () { C c ; kasta c ; // kopiering av det namngivna objektet c till undantagsobjektet. } // Det är oklart om denna kopia kan tas bort (utelämnas). int main () { try { f (); } catch ( C c ) { // kopiering av undantagsobjektet till det temporära i // undantagsdeklarationen. } // Det är också oklart om denna kopia kan tas bort (utelämnas). }
En överensstämmande kompilator bör därför producera ett program som skriver ut "Hello World!" dubbelt. I den aktuella revideringen av C++-standarden ( C++11 ) har problemen åtgärdats, vilket i huvudsak tillåter att både kopian från det namngivna objektet till undantagsobjektet och kopian till objektet som deklarerats i undantagshanteraren kan elimineras.
GCC tillhandahåller alternativet -fno-elide-constructors
för att inaktivera copy-elision. Det här alternativet är användbart för att observera (eller inte observera) effekterna av returvärdeoptimering eller andra optimeringar där kopior försvinner. Det rekommenderas i allmänhet inte att inaktivera denna viktiga optimering.
Returvärdeoptimering
I samband med programmeringsspråket C++ är returvärdeoptimering ( RVO ) en kompilatoroptimering som innebär att man eliminerar det tillfälliga objektet som skapats för att hålla en funktions returvärde. RVO tillåts ändra det observerbara beteendet för det resulterande programmet enligt C++-standarden .
Sammanfattning
I allmänhet tillåter C++-standarden en kompilator att utföra vilken optimering som helst, förutsatt att den resulterande körbara filen uppvisar samma observerbara beteende som om (dvs låtsas) alla krav i standarden har uppfyllts. Detta kallas vanligtvis "som -om-regeln ". Termen returvärdeoptimering syftar på en speciell klausul i C++-standarden som går ännu längre än "som-om"-regeln: en implementering kan utelämna en kopieringsoperation som är ett resultat av en return-sats , även om kopieringskonstruktorn har bieffekter .
Följande exempel visar ett scenario där implementeringen kan eliminera en eller båda kopiorna som görs, även om kopieringskonstruktorn har en synlig bieffekt (utskrift av text). Den första kopian som kan elimineras är den där ett namnlöst temporärt C
skulle kunna kopieras till funktionen fs
returvärde . Den andra kopian som kan tas bort är kopian av det temporära objektet som returneras av f
till obj
.
#include <iostream> struct C { C () = default ; C ( const C & ) { std :: cout << "En kopia gjordes. \n " ; } }; C f () { returnera C (); } int main () { std :: cout << "Hej världen! \n " ; C obj = f (); }
Beroende på kompilatorn och den kompilatorns inställningar kan det resulterande programmet visa någon av följande utdata:
Hej världen! En kopia gjordes. En kopia gjordes.
Hej världen! En kopia gjordes.
Hej världen!
Bakgrund
Att returnera ett objekt av inbyggd typ från en funktion medför vanligtvis liten eller ingen omkostnad, eftersom objektet vanligtvis passar i ett CPU-register . Att returnera ett större objekt av klasstyp kan kräva dyrare kopiering från en minnesplats till en annan. För att undvika detta kan en implementering skapa ett dolt objekt i anroparens stackram och skicka adressen till detta objekt till funktionen. Funktionens returvärde kopieras sedan till det dolda objektet. Alltså kod som denna:
struct Data { char bytes [ 16 ]; }; Data F () { Dataresultat = { }; // generera resultat returnera resultat ; } int main () { Data d = F (); }
kan generera kod motsvarande detta:
struct Data { char bytes [ 16 ]; }; Data * F ( Data * _hiddenAddress ) { Dataresultat = { }; // kopiera resultatet till dolt objekt * _hiddenAddress = resultat ; returnera _hiddenAddress ; } int main () { Data _hidden ; // skapa dolt objekt Data d = * F ( & _hidden ); // kopiera resultatet till d }
vilket gör att Data-
objektet kopieras två gånger.
I de tidiga stadierna av utvecklingen av C++ ansågs språkets oförmåga att effektivt returnera ett objekt av klasstyp från en funktion som en svaghet. Runt 1991 implementerade Walter Bright en teknik för att minimera kopiering, genom att effektivt ersätta det dolda objektet och det namngivna objektet inuti funktionen med objektet som användes för att hålla resultatet:
struct Data { char bytes [ 16 ]; }; void F ( Data * p ) { // generera resultat direkt i *p } int main () { Data d ; F ( & d ); }
Bright implementerade denna optimering i sin Zortech C++ kompilator. Den här speciella tekniken myntades senare som "Named return value optimization" (NRVO), med hänvisning till det faktum att kopieringen av ett namngivet objekt elimineras.
Kompilatorstöd
Returvärdesoptimering stöds på de flesta kompilatorer. Det kan dock finnas omständigheter där kompilatorn inte kan utföra optimeringen. Ett vanligt fall är när en funktion kan returnera olika namngivna objekt beroende på körningsvägen:
#include <string> std :: sträng F ( bool cond = false ) { std :: sträng först ( "första" ); std :: sträng sekund ( "andra" ); // funktionen kan returnera ett av två namngivna objekt // beroende på dess argument. RVO kanske inte tillämpas returvillkor ? första : andra ; } int main () { std :: strängresultat = F () ; }