Sök…


Syfte med kopiering elision

Det finns platser i standarden där ett objekt kopieras eller flyttas för att initiera ett objekt. Copy elision (ibland kallad return value optimization) är en optimering där en kompilator under vissa specifika omständigheter tillåter att undvika kopian eller flytta även om standarden säger att det måste hända.

Tänk på följande funktion:

std::string get_string()
{
  return std::string("I am a string.");
}

Enligt den stränga formuleringen i standarden kommer denna funktion att initialisera en tillfällig std::string , kopiera / flytta sedan den till returvärdesobjektet och förstöra det temporära. Standarden är mycket tydlig att det är så att koden tolkas.

Copy elision är en regel som tillåter en C ++ -kompilerare att ignorera skapandet av det temporära och dess efterföljande kopia / förstörelse. Det vill säga kompilatorn kan ta initialiseringsuttrycket för det tillfälliga och initiera funktionens returvärde direkt från det. Detta sparar uppenbarligen prestanda.

Det har dock två synliga effekter på användaren:

  1. Typen måste ha den kopia / flytta konstruktör som skulle ha kallats. Även om kompilatorn undviker att kopiera / flytta måste typen fortfarande kunna kopieras / flyttas.

  2. Biverkningar av kopierings- / flyttkonstruktörer garanteras inte under omständigheter där elision kan ske. Tänk på följande:

C ++ 11
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();
}

Vad kommer att ringa func göra? Det kommer aldrig att skriva ut "Kopiering", eftersom det tillfälliga är en rvalue och my_type är en rörlig typ. Så kommer det att skriva ut "Moving"?

Utan kopieringselisionsregeln skulle detta krävas för att alltid skriva ut "Flytta". Men eftersom kopieringselisionsregeln existerar kan flyttkonstruktören kallas eller inte; det är implementeringsberoende.

Och därför kan du inte vara beroende av att kopia / flytta konstruktörer ringer i sammanhang där det är möjligt att använda.

Eftersom elision är en optimering kanske din kompilator inte stöder elision i alla fall. Och oavsett om kompilatorn eliminerar ett visst fall eller inte, måste typen fortfarande stödja operationen som elimineras. Så om en kopieringskonstruktion undviks, måste typen fortfarande ha en kopieringskonstruktör, även om den inte kommer att kallas.

Garanterad kopia elision

C ++ 17

Normalt är elision en optimering. Även om praktiskt taget alla kompilatorer stöder kopieringselision i de enklaste fallen, har elision fortfarande en viss börda för användare. Den typen som kopierar / flyttar måste dock fortfarande ha den kopierings- / flyttningsoperation som har undanröjts.

Till exempel:

std::mutex a_mutex;
std::lock_guard<std::mutex> get_lock()
{
  return std::lock_guard<std::mutex>(a_mutex);
}

Detta kan vara användbart i fall där a_mutex är en mutex som privat hålls av något system, men en extern användare kanske vill ha en scoped-lås till det.

Detta är inte heller lagligt eftersom std::lock_guard inte kan kopieras eller flyttas. Trots att praktiskt taget alla C ++ -kompilatorer kommer att undanröja kopieringen / flytta, kräver standarden fortfarande att typen har den operationen tillgänglig.

Tills C ++ 17.

C ++ 17 kräver elision genom att effektivt omdefiniera själva betydelsen av vissa uttryck så att ingen kopia / rörelse äger rum. Tänk på ovanstående kod.

Under ordalydelse före C ++ 17 säger den koden att skapa en tillfällig och sedan använda den temporära för att kopiera / flytta till returvärdet, men den tillfälliga kopian kan tas bort. Under C ++ 17-formulering skapar det inte en tillfällig alls.

I C ++ 17 genererar inte något uttryck uttryck , när det används för att initialisera ett objekt av samma typ som uttrycket, ett tillfälligt. Uttrycket initialiserar objektet direkt. Om du returnerar ett värde av samma typ som returvärdet, behöver typen inte ha en kopia / flytta konstruktör. Och därför, enligt C ++ 17-regler, kan koden ovan fungera.

C ++ 17-formuleringen fungerar i de fall prvalans typ stämmer med den typ som initieras. Så givet get_lock ovan kommer detta inte heller att kräva en kopia / flytta:

std::lock_guard the_lock = get_lock();

Eftersom resultatet av get_lock är ett uttryck som används för att initialisera ett objekt av samma typ kommer ingen kopiering eller flyttning att ske. Detta uttryck skapar aldrig en tillfällig; den används för att direkt initiera the_lock . Det finns ingen elision eftersom det inte finns någon kopia / flytta för att bli elided elide.

Uttrycket "garanterad kopieringselision" är därför något av en felaktig namn, men det är namnet på funktionen eftersom det föreslås för C ++ 17-standardisering . Det garanterar inte elision alls. det eliminerar kopian / flytt helt och hållet, och omdefinierar C ++ så att det aldrig fanns en kopia / flytt som skulle undvikas.

Den här funktionen fungerar endast i fall där ett uttryck för ett värde används. Som sådan använder detta de vanliga reglerna för elision:

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;
}

Även om detta är en giltig fråga för kopiering elision behöver C ++ 17 reglerna inte eliminera kopiera / flytta i detta fall. Som sådan måste typen fortfarande ha en kopia / flytta konstruktör att använda för att initiera returvärdet. Och eftersom lock_guard inte gör det är detta fortfarande ett kompileringsfel. Implementeringar tillåter att vägra att ta bort kopior när man skickar eller returnerar ett objekt av trivialt kopierbar typ. Detta är för att tillåta att sådana objekt flyttas runt i register, vilket vissa ABI: er kan kräva i sina samtalskonventioner.

struct trivially_copyable {
    int a;  
};

void foo (trivially_copyable a) {}

foo(trivially_copyable{}); //copy elision not mandated

Returvärde elision

Om du returnerar ett förhandsuttryck från en funktion, och utskriften har samma typ som funktionens återgångstyp, kan kopian från tillfälligt förval undvikas:

std::string func()
{
  return std::string("foo");
}

Ganska mycket alla kompilatorer kommer att undanröja den tillfälliga konstruktionen i detta fall.

Parameter elision

När du skickar ett argument till en funktion, och argumentet är ett förhandsuttryck för funktionens parametertyp, och den här typen är inte en referens, kan prvaluets konstruktion undvikas.

void func(std::string str) { ... }

func(std::string("foo"));

Detta säger att skapa en tillfällig string , flytta den sedan till funktionsparametern str . Copy elision tillåter detta uttryck att direkt skapa objektet i str , snarare än att använda ett tillfälligt + drag.

Detta är en användbar optimering för fall där en konstruktör förklaras explicit . Vi kan till exempel ha skrivit ovanstående som func("foo") , men bara för att string har en implicit konstruktör som konverterar från en const char* till en string . Om den konstruktören var explicit skulle vi tvingas använda en tillfällig för att ringa den explicit konstruktören. Copy elision sparar oss från att behöva göra en onödig kopia / flytta.

Namngivet returvärde elision

Om du returnerar ett lvalue-uttryck från en funktion och detta lvalue:

  • representerar en automatisk variabel lokal för den funktionen som kommer att förstöras efter return
  • den automatiska variabeln är inte en funktionsparameter
  • och variabeltypen är samma typ som funktionens returtyp

Om alla dessa är fallet, kan kopian / flyttningen från lvaluen undanröjas:

std::string func()
{
  std::string str("foo");
  //Do stuff
  return str;
}

Mer komplexa fall är berättigade till elision, men ju mer komplex fallet är, desto mindre troligt kommer kompilatorn att faktiskt undanröja det:

std::string func()
{
  std::string ret("foo");
  if(some_condition)
  {
    return "bar";
  }
  return ret;
}

Kompilatorn kan fortfarande undanröja ret , men chansen att de gör det går ner.

Som tidigare nämnts är elision inte tillåtet för värdeparametrar.

std::string func(std::string str)
{
  str.assign("foo");
  //Do stuff
  return str; //No elision possible
}

Kopiera initialiseringselision

Om du använder ett förhandsuttryck för att kopiera initiera en variabel, och den variabeln har samma typ som uttrycket för förvalta, kan kopieringen undvikas.

std::string str = std::string("foo");

Kopieringsinitialisering omvandlar detta effektivt till std::string str("foo"); (det finns mindre skillnader).

Detta fungerar också med returvärden:

std::string func()
{
  return std::string("foo");
}

std::string str = func();

Utan kopia elision, skulle detta provocera 2 samtal till std::string flytta konstruktör. Copy elision tillåter detta att ringa flyttkonstruktören 1 eller noll gånger, och de flesta kompilatorer kommer att välja den senare.



Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow