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

377

¿Qué es copiar elisión? ¿Qué es la optimización de valor de retorno (con nombre)? ¿Qué implican?

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

Luchian Grigore
fuente
1
Copiar elision es una forma de verlo; Elisión de objetos o fusión de objetos (o confusión) es otra vista.
curioso
Encontré este enlace útil.
subtleseeker

Respuestas:

246

Introducción

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

Para casos comunes donde se produce una elisión de copia, salte a esta respuesta .

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

Es la única forma de optimización que elude (¡ja!) La regla de si-como: la copia de elisión puede aplicarse 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, las siguientes salidas son todas válidas :

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 debe tener lógica crítica dentro de los constructores o destructores de copia / movimiento, ya que no puede confiar en que se les llame.

Si se elude una llamada a un constructor de copia o movimiento, 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:

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
fuente
2
¿podría explicar cuándo se produce la segunda salida y cuándo ocurre la tercera?
zhangxaochen
3
@zhangxaochen cuándo y cómo el compilador decide optimizar de esa manera.
Luchian Grigore
10
@zhangxaochen, primera salida: la copia 1 es del retorno a una temperatura y la copia 2 de la temperatura a obj; El segundo es cuando uno de los anteriores se optimiza, probablemente se elimina la copia de devolución; la thris ambos están elididas
Victor
2
Hmm, pero en mi opinión, esta DEBE ser una característica en la que podemos confiar. Porque si no podemos, afectaría severamente la forma en que implementamos nuestras funciones en C ++ moderno (RVO vs std :: move). Al ver algunos de los videos de CppCon 2014, realmente tuve la impresión de que todos los compiladores modernos siempre hacen RVO. Además, he leído en alguna parte que también sin ninguna optimización, los compiladores lo aplican. Pero, por supuesto, no estoy seguro de eso. Por eso estoy preguntando.
j00hi
8
@ j00hi: nunca escriba move en una declaración de devolución; si no se aplica rvo, el valor de retorno se moverá de manera predeterminada de todos modos.
MikeMB
96

Referencia estándar

Para una visión e introducción menos técnica, salte a esta respuesta .

Para casos comunes donde se produce una elisión de copia, salte a esta respuesta .

Copiar elisión 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 copia / movimiento del objeto tiene efectos secundarios. En tales casos, la implementación trata el origen y el destino de la operación omitida de copiar / mover como simplemente dos formas diferentes de referirse al mismo objeto, y la destrucción de ese objeto ocurre en el último momento en que los dos objetos habrían sido destruido sin la optimización. 123 Esta elisión de operaciones de copiar / mover, llamada copia de elisión , está permitida en las siguientes circunstancias (que pueden combinarse para eliminar múltiples copias):

- 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 parámetro de cláusula catch) con el mismo tipo cvunqualified que el tipo de retorno de 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 parámetro de cláusula catch) cuyo alcance no se extiende más allá del final del bloque de prueba que lo encierra 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 se ha vinculado a una referencia (12.2) se copiará / moverá a un objeto de clase con el mismo tipo no calificado por cv, la operación de copiar / mover se puede omitir construyendo el objeto temporal directamente en el objetivo de la copia / movimiento omitido

- cuando la declaración de excepción de un manejador de excepción (Cláusula 15) declara un objeto del mismo tipo (excepto para la calificación cv) como el objeto de excepción (15.1), la operación de copiar / mover puede omitirse tratando la declaración de excepción como un 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 copia / movimiento, 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 para la 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, la construcción del objeto local t puede verse como una inicialización directa del objeto global t2, y la destrucción de ese objeto ocurrirá a la salida del programa. Agregar un constructor de movimiento a Thing tiene el mismo efecto, pero es la construcción de movimiento desde el objeto temporal hasta la t2que se elide.

Luchian Grigore
fuente
1
¿Es del estándar C ++ 17 o de una versión anterior?
Nils
90

Formas comunes de elisión de copia

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

Para una visión e introducción menos técnica, salte a esta respuesta .

(Nombre) 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 el valor de un método tiene su copia omitida. El ejemplo expuesto 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 del valor de retorno regular ocurre cuando se devuelve un temporal:

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

Otros lugares comunes donde se realiza la elisión de copia es cuando se pasa un valor temporal por valor :

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

foo(Thing());

o cuando una excepción es lanzada y capturada 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 grado comercial admiten copia de elisión y (N) RVO (según la configuración de optimización).

Luchian Grigore
fuente
44
Me interesaría ver las viñetas de "Limitaciones comunes" explicadas solo un poco ... ¿Qué hace que estos factores limitantes?
phonetagger
@phonetagger Me vinculé con el artículo de msdn, espero que aclare algunas cosas.
Luchian Grigore
54

Copy elision 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 de 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 una duración de almacenamiento automática (que no es un parámetro de función), entonces la copia / mover eso sería realizado por un compilador no optimizador puede omitirse. Si es así, el valor devuelto se construye directamente en el almacenamiento al que, de lo contrario, el valor devuelto de la función se movería o copiaría.
  2. RVO (Optimización del valor de retorno) : si la función devuelve un objeto temporal sin nombre que sería movido o copiado en el destino por un compilador ingenuo, 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());//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 realiza una elisión de copia y no se llama al constructor de copia / movimiento, debe estar presente y accesible (como si no hubiera sucedido ninguna optimización), de lo contrario, el programa está mal formado.

Debe permitir dicha copia de elisión solo en lugares donde no afecte el comportamiento observable de su software. Copiar elisión es la única forma de optimización que permite tener (es decir, eludir) 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 deshabilitar la copia de elisión. Si desea evitar una posible elisión de copia, úsela -fno-elide-constructors.

Ahora, casi todos los compiladores proporcionan copia de elisión 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, ahorrando así espacio en el marco de la pila.

Ajay yadav
fuente
66
La declaración ABC obj2(xyz123());es NRVO o RVO? ¿no está obteniendo una variable / objeto temporal igual que ABC xyz = "Stack Overflow";//RVO
Asif Mushtaq
3
Para tener una ilustración más concreta de RVO, puede consultar el ensamblaje que genera el compilador (cambie la bandera del compilador -fno-elide-constructors para ver la diferencia). godbolt.org/g/Y2KcdH
Gab 是 好人