Что такое оптимизация копирования и возвращаемое значение?

Что такое копирование? Что такое (именованная) оптимизация возвращаемого значения? Что они подразумевают?

В каких ситуациях они могут возникнуть? Каковы ограничения?

4 ответа

Решение

Вступление

Для технического обзора - перейдите к этому ответу.

Для общих случаев, когда происходит удаление копии - перейдите к этому ответу.

Копирование elision - это оптимизация, реализованная большинством компиляторов для предотвращения лишних (потенциально дорогих) копий в определенных ситуациях. Это делает возврат по стоимости или передачу по стоимости осуществимым на практике (применяются ограничения).

Это единственная форма оптимизации, которая исключает (ха!) Правило "как будто" - исключение копирования может применяться, даже если копирование / перемещение объекта имеет побочные эффекты.

Следующий пример взят из Википедии:

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

В зависимости от компилятора и настроек допустимы следующие выходные данные:

Привет, мир!
Копия была сделана.
Копия была сделана.


Привет, мир!
Копия была сделана.


Привет, мир!

Это также означает, что меньше объектов может быть создано, поэтому вы также не можете полагаться на определенное количество вызываемых деструкторов. У вас не должно быть критической логики внутри конструкторов копирования / перемещения или деструкторов, поскольку вы не можете полагаться на их вызов.

Если вызов конструктора копирования или перемещения исключен, этот конструктор должен все еще существовать и быть доступным. Это гарантирует, что исключение копирования не позволяет копировать объекты, которые обычно не копируются, например, потому что они имеют закрытый или удаленный конструктор копирования / перемещения.

C++ 17: Начиная с C++17, Copy Elision гарантируется, когда объект возвращается напрямую:

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
}

Распространенные формы исключения

Для технического обзора - перейдите к этому ответу.

Для менее технического представления и ознакомления - перейдите к этому ответу.

(Named) Оптимизация возвращаемого значения является распространенной формой исключения копирования. Это относится к ситуации, когда объект, возвращаемый значением из метода, получает свою копию. Пример, изложенный в стандарте, иллюстрирует оптимизацию именованного возвращаемого значения, поскольку объект назван.

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

Регулярная оптимизация возвращаемого значения происходит при возвращении временного:

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

Другие распространенные места, где исключение копии имеет место, когда временное значение передается по значению:

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

foo(Thing());

или когда исключение выдается и перехватывается значением:

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

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

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

Общие ограничения права на копирование:

  • несколько точек возврата
  • условная инициализация

Большинство компиляторов коммерческого уровня поддерживают копирование elision & (N)RVO (в зависимости от настроек оптимизации).

Стандартная ссылка

Для менее технического представления и ознакомления - перейдите к этому ответу.

Для общих случаев, когда происходит удаление копии - перейдите к этому ответу.

Копия выбрана в стандарте в:

12.8 Копирование и перемещение объектов класса [class.copy]

как

31) При выполнении определенных критериев реализация может опустить конструкцию копирования / перемещения объекта класса, даже если конструктор копирования / перемещения и / или деструктор для объекта имеют побочные эффекты. В таких случаях реализация рассматривает источник и цель пропущенной операции копирования / перемещения просто как два разных способа обращения к одному и тому же объекту, и уничтожение этого объекта происходит в более поздние времена, когда два объекта были бы уничтожен без оптимизации.123 Это исключение операций копирования / перемещения, называемых разрешением копирования, допускается при следующих обстоятельствах (которые могут быть объединены для устранения нескольких копий):

- в операторе возврата в функции с типом возврата класса, когда выражение является именем энергонезависимого автоматического объекта (кроме параметра функции или предложения catch) с тем же cvunqualified типом, что и тип возврата функции, Операция копирования / перемещения может быть опущена путем создания автоматического объекта непосредственно в возвращаемое значение функции

- в выражении throw, когда операндом является имя энергонезависимого автоматического объекта (отличного от параметра функции или предложения catch), область которого не выходит за пределы самого внутреннего включающего блока try (если есть 1), операция копирования / перемещения из операнда в объект исключения (15.1) может быть опущена путем создания автоматического объекта непосредственно в объект исключения

- когда временный объект класса, который не был связан со ссылкой (12.2), будет скопирован / перемещен в объект класса с тем же типом cv-unqualified, операция копирования / перемещения может быть опущена путем создания временного объекта непосредственно в цель пропущенного копирования / перемещения

- когда объявление исключения в обработчике исключений (раздел 15) объявляет объект того же типа (за исключением cv-квалификации), что и объект исключения (15.1), операция копирования / перемещения может быть опущена путем обработки объявления исключения в качестве псевдонима для объекта исключения, если значение программы не изменится, за исключением выполнения конструкторов и деструкторов для объекта, объявленного объявлением исключения.

123) Поскольку уничтожается только один объект вместо двух, а один конструктор копирования / перемещения не выполняется, для каждого созданного объекта все еще остается один уничтоженный объект.

Пример приведен ниже:

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

и объяснил:

Здесь критерии для исключения могут быть объединены, чтобы исключить два вызова конструктора копирования класса Thing: копирование локального автоматического объекта t во временный объект для возвращаемого значения функции f() и копирование этого временного объекта в объект t2, Эффективно, строительство местного объекта t может рассматриваться как непосредственная инициализация глобального объекта t2и уничтожение этого объекта произойдет при выходе из программы. Добавление конструктора перемещения в Thing имеет тот же эффект, но это конструкция перемещения из временного объекта в t2 это исключено.

Copy elision - это метод оптимизации компилятора, который исключает ненужное копирование / перемещение объектов.

В следующих обстоятельствах компилятору разрешено пропускать операции копирования / перемещения и, следовательно, не вызывать связанный конструктор:

  1. NRVO (оптимизация именованных возвращаемых значений): если функция возвращает тип класса по значению, а выражением оператора возврата является имя энергонезависимого объекта с автоматической продолжительностью хранения (который не является параметром функции), то копирование / перемещение это может быть выполнено неоптимизирующим компилятором и может быть опущено. Если это так, возвращаемое значение создается непосредственно в хранилище, в которое в противном случае возвращаемое значение функции было бы перемещено или скопировано.
  2. RVO (Оптимизация возвращаемого значения): если функция возвращает безымянный временный объект, который был бы перемещен или скопирован в место назначения наивным компилятором, копирование или перемещение могут быть опущены в соответствии с 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());//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  

Даже когда удаление копии происходит и конструктор копирования / перемещения не вызывается, он должен присутствовать и быть доступным (как если бы оптимизация вообще не происходила), в противном случае программа не сформирована.

Вы должны разрешить такое копирование только в тех местах, где это не повлияет на наблюдаемое поведение вашего программного обеспечения. Исключение копирования является единственной формой оптимизации, которой разрешено иметь (т.е. исключать) наблюдаемые побочные эффекты. Пример:

#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 обеспечивает -fno-elide-constructors возможность отключить копирование elision. Если вы хотите избежать возможного копирования, используйте -fno-elide-constructors,

Теперь почти все компиляторы предоставляют разрешение на копирование, когда оптимизация включена (и если никакая другая опция не установлена, чтобы отключить ее).

Заключение

При каждом исключении копии одна конструкция и одно соответствующее уничтожение копии опускаются, что экономит время ЦП, а один объект не создается, тем самым экономя место на фрейме стека.

Здесь я привожу еще один пример копирования, с которым я, по-видимому, столкнулся сегодня.

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

}

В результате:

In   Obj()
In   Obj()
assignment:
In   Obj(const Obj & org)
Obj_instance2.var1:3
Другие вопросы по тегам