¿Qué son la elisión de copia y la optimización del valor de retorno?

Resuelto Luchian Grigore asked hace 12 años • 5 respuestas

¿Qué es la elisión de copia? ¿Qué es la optimización del valor de retorno (denominada)? ¿Qué implican?

¿En qué situaciones pueden ocurrir? ¿Qué son las limitaciones?

  • Si se le hizo referencia a esta pregunta, probablemente esté buscando la introducción .
  • Para obtener una descripción técnica, consulte la referencia estándar .
  • Vea casos comunes aquí .
Luchian Grigore avatar Oct 18 '12 18:10 Luchian Grigore
Aceptado

Introducción

Para obtener una descripción técnica, salte a esta respuesta .

Para casos comunes en los que se produce elisión de copia, pase a esta respuesta .

La elisión de copia es una optimización implementada por la mayoría de los compiladores para evitar copias adicionales (potencialmente costosas) en determinadas situaciones. Hace que la devolución por valor o el paso por valor sea factible en la práctica (se aplican restricciones).

Es la única forma de optimización que elide (¡ja!) la regla como si: la elisión de copia se puede aplicar incluso si copiar/mover el objeto tiene efectos secundarios .

El siguiente ejemplo tomado de 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();
}

Dependiendo del compilador y la configuración, los siguientes resultados son válidos :

¡Hola Mundo!
Se hizo una copia.
Se hizo una copia.


¡Hola Mundo!
Se hizo una copia.


¡Hola Mundo!

Esto también significa que se pueden crear menos objetos, por lo que tampoco puede confiar en que se llame a un número específico de destructores. No debería tener lógica crítica dentro de los constructores o destructores de copiar/mover, ya que no puede confiar en que los llamen.

Si se omite una llamada a un constructor de copiar o mover, ese constructor aún debe existir y debe ser accesible. Esto garantiza que la elisión de copia no permita copiar objetos que normalmente no se pueden copiar, por ejemplo, porque tienen un constructor de copia/movimiento privado o eliminado.

C++17 : a partir de C++17, Copy Elision está garantizado cuando un objeto se devuelve directamente y, en este caso, no es necesario que el constructor de copia o movimiento esté accesible o esté presente:

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
}
Luchian Grigore avatar Oct 18 '2012 11:10 Luchian Grigore

Formas comunes de elisión de copia

Para obtener una descripción técnica, salte a esta respuesta .

Para una visión y una introducción menos técnicas, salte a esta respuesta .

(Nombrado) La optimización del valor de retorno es una forma común de elisión de copia. Se refiere a la situación en la que un objeto devuelto por valor de un método tiene su copia elidida. El ejemplo establecido en el estándar ilustra la optimización del valor de retorno con nombre , ya que el objeto tiene nombre.

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

La optimización regular del valor de retorno se produce cuando se devuelve un temporal:

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

Otros lugares comunes donde tiene lugar la elisión de copia es cuando un objeto se construye a partir de un temporal :

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

Thing t2 = Thing();
Thing t3 = Thing(Thing()); // two rounds of elision
foo(Thing()); // parameter constructed from temporary

o cuando se lanza una excepción y se detecta por valor :

struct Thing{
  Thing();
  Thing(const Thing&);
};
 
void foo() {
  Thing c;
  throw c;
}
 
int main() {
  try {
    foo();
  }
  catch(Thing c) {  
  }             
}

Las limitaciones comunes de la elisión de copia son:

  • múltiples puntos de retorno
  • inicialización condicional

La mayoría de los compiladores de nivel comercial admiten la elisión de copia y (N)RVO (según la configuración de optimización). C++ 17 hace que muchas de las clases de elisión de copia anteriores sean obligatorias.

Luchian Grigore avatar Oct 18 '2012 11:10 Luchian Grigore

Referencia estándar

Para una visión y una introducción menos técnicas, salte a esta respuesta .

Para casos comunes en los que se produce elisión de copia, pase a esta respuesta .

La elisión de copia se define en el estándar en:

12.8 Copiar y mover objetos de clase [class.copy]

como

31) Cuando se cumplen ciertos criterios, una implementación puede omitir la construcción de copiar/mover de un objeto de clase, incluso si el constructor y/o destructor de copiar/mover del objeto tiene efectos secundarios. En tales casos, la implementación trata el origen y el destino de la operación de copiar/mover omitida como simplemente dos formas diferentes de referirse al mismo objeto, y la destrucción de ese objeto ocurre en el último de los momentos en que los dos objetos habrían sido destruido sin la optimización. 123 Esta elisión de operaciones de copiar/mover, llamada elisión de copia , está permitida en las siguientes circunstancias (que pueden combinarse para eliminar copias múltiples):

— en una declaración de retorno en una función con un tipo de retorno de clase, cuando la expresión es el nombre de un objeto automático no volátil (que no sea una función o un parámetro de cláusula de captura) con el mismo tipo cvunqualified que el tipo de retorno de la función, el La operación de copiar/mover se puede omitir construyendo el objeto automático directamente en el valor de retorno de la función.

— en una expresión de lanzamiento, cuando el operando es el nombre de un objeto automático no volátil (que no sea una función o un parámetro de cláusula de captura) cuyo alcance no se extiende más allá del final del bloque try más interno (si hay uno), la operación de copiar/mover del operando al objeto de excepción (15.1) se puede omitir construyendo el objeto automático directamente en el objeto de excepción

— cuando un objeto de clase temporal que no ha sido vinculado a una referencia (12.2) se copia/mueve a un objeto de clase con el mismo tipo cv no calificado, la operación de copiar/mover se puede omitir construyendo el objeto temporal directamente en el destino de la copia/mover omitido

— cuando la declaración de excepción de un manejador de excepciones (Cláusula 15) declara un objeto del mismo tipo (excepto la calificación cv) que el objeto de excepción (15.1), la operación copiar/mover se puede omitir tratando la declaración de excepción como alias para el objeto de excepción si el significado del programa no cambiará excepto por la ejecución de constructores y destructores para el objeto declarado por la declaración de excepción.

123) Debido a que solo se destruye un objeto en lugar de dos, y no se ejecuta un constructor de copiar/mover, todavía hay un objeto destruido por cada uno construido.

El ejemplo dado es:

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

y explicó:

Aquí los criterios de elisión se pueden combinar para eliminar dos llamadas al constructor de copia de la clase Thing: la copia del objeto automático local ten el objeto temporal para el valor de retorno de la función f() y la copia de ese objeto temporal en el objeto t2. Efectivamente, t se puede considerar que la construcción del objeto local inicializa directamente el objeto global t2, y la destrucción de ese objeto se producirá al salir del programa. Agregar un constructor de movimiento a Thing tiene el mismo efecto, pero es la construcción de movimiento del objeto temporal a t2la que se omite.

Luchian Grigore avatar Oct 18 '2012 11:10 Luchian Grigore

La elisión de copia es una técnica de optimización del compilador que elimina la copia/movimiento innecesario de objetos.

En las siguientes circunstancias, un compilador puede omitir operaciones de copiar/mover y, por lo tanto, no llamar al constructor asociado:

  1. NRVO (Optimización del valor de retorno con nombre) : si una función devuelve un tipo de clase por valor y la expresión de la declaración de retorno es el nombre de un objeto no volátil con duración de almacenamiento automático (que no es un parámetro de función), entonces copiar/mover que sería realizado por un compilador no optimizado se puede omitir. Si es así, el valor devuelto se construye directamente en el almacenamiento al que de otro modo se movería o copiaría el valor de retorno de la función.
  2. RVO (Optimización del valor de retorno) : si la función devuelve un objeto temporal sin nombre que un compilador ingenuo movería o copiaría al destino, la copia o el movimiento se pueden omitir según 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  

Incluso cuando se produce la elisión de copia y no se llama al constructor copiar/mover, debe estar presente y ser accesible (como si no hubiera habido ninguna optimización); de lo contrario, el programa está mal formado.

Debe permitir dicha elisión de copia sólo en lugares donde no afecte el comportamiento observable de su software. La elisión de copia es la única forma de optimización a la que se le permite tener (es decir, eliminar) efectos secundarios observables. Ejemplo:

#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 ofrece la -fno-elide-constructorsopción de desactivar la elisión de copia. Si desea evitar una posible elisión de copia, utilice -fno-elide-constructors.

Ahora casi todos los compiladores proporcionan elisión de copia cuando la optimización está habilitada (y si no hay otra opción configurada para deshabilitarla).

Conclusión

Con cada elisión de copia, se omite una construcción y una destrucción coincidente de la copia, lo que ahorra tiempo de CPU, y no se crea un objeto, lo que ahorra espacio en el marco de la pila.

Ajay yadav avatar Jan 13 '2015 07:01 Ajay yadav