c++ - ¿Qué son la elección de copia y la optimización del valor de retorno?
optimization c++-faq (4)
Formas comunes de copia elision
Para obtener una descripción técnica, pase a esta respuesta .
Para una vista 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 elision de copia. Se refiere a la situación en la que un objeto devuelto por valor de un método tiene su copia eliminada. El ejemplo establecido en el estándar ilustra la optimización del valor de retorno nombrado , ya que el objeto es nombrado.
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 tiene lugar la elección de copia es cuando se pasa un valor temporal por :
class Thing {
public:
Thing();
~Thing();
Thing(const Thing&);
};
void foo(Thing t);
foo(Thing());
o cuando una excepción es lanzada y atrapada 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 elección de copia son:
- múltiples puntos de retorno
- inicialización condicional
La mayoría de los compiladores de nivel comercial admiten elision de copia y (N) RVO (según la configuración de optimización).
¿Qué es copia elision? ¿Qué es (con nombre) la optimización del valor de retorno? ¿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 una descripción técnica, vea la referencia estándar .
- Ver casos comunes aquí .
Introducción
Para obtener una descripción técnica, pase a esta respuesta .
Para los casos comunes en los que se produce una copia de la copia: pase a esta respuesta .
Copy elision es una optimización implementada por la mayoría de los compiladores para evitar copias adicionales (potencialmente caras) en ciertas situaciones. En la práctica, la devolución por valor o paso por valor es factible (se aplican restricciones).
Es la única forma de optimización que elimina (¡ja!) La regla de "si": se puede aplicar elision de copia 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 de copia / movimiento o destructores, ya que no puede confiar en que se los llame.
Si se elimina una llamada a un constructor de copia o movimiento, dicho constructor aún debe existir y debe ser accesible. Esto garantiza que elision 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
}
Referencia estándar
Para una vista y una introducción menos técnicas, salte a esta respuesta .
Para los casos comunes en los que se produce una copia de la copia: pase a esta respuesta .
Copia elision se define en la norma en:
12.8 Copiar y mover objetos de clase [class.copy]
como
31) Cuando se cumplen ciertos criterios, se permite que una implementación omita 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 de copia / movimiento omitida simplemente como 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 hubieran sido Destruido sin la optimización. 123 Esta elección de operaciones de copia / movimiento, llamada copia de la elección , se permite en las siguientes circunstancias (que pueden combinarse para eliminar varias 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 un parámetro de función o 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 (distinto de un parámetro de función o de cláusula catch) cuyo alcance no se extiende más allá del final del bloque de intento de cierre más interno (si existe) uno), la operación de copiar / mover del operando al objeto de excepción (15.1) se puede omitir al construir 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 copiaría / movería a un objeto de clase con el mismo tipo no cualificado de 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 controlador de excepciones (Cláusula 15) declara un objeto del mismo tipo (excepto para la calificación cv) que el objeto de excepción (15.1), la operación de copiar / mover se puede omitir al tratar la declaración de excepción como un alias para el objeto de excepción si el significado del programa no cambia, 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í se pueden combinar los criterios de elección para eliminar dos llamadas al constructor de copia de la clase
Thing
: la copia del objeto automático localt
en el objeto temporal para el valor de retorno de la funciónf()
y la copia de ese objeto temporal en el objetot2
Efectivamente, la construcción del objeto localt
puede verse como una inicialización directa del objeto globalt2
, y la destrucción de ese objeto se producirá al salir del programa. Agregar un constructor de movimiento a Cosa tiene el mismo efecto, pero lo que se elimina es la construcción de movimiento del objeto temporal at2
.
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 las operaciones de copiar / mover y, por tanto, no llamar al constructor asociado:
- 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 duración de almacenamiento automático (que no es un parámetro de función), entonces la copia / movimiento que se llevaría a cabo por un compilador que no optimiza puede omitirse. Si es así, el valor devuelto se construye directamente en el almacenamiento al cual se movería o copiaría el valor de retorno de la función.
- 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 en el destino, la copia o el movimiento se puede omitir como en 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 = "";//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 copia de elision y no se llama al constructor de copia / movimiento, debe estar presente y accesible (como si no se hubiera realizado ninguna optimización), de lo contrario, el programa no está configurado correctamente.
Debe permitir dicha elección de copias solo en lugares donde no afectará el comportamiento observable de su software. Copy elision es la única forma de optimización permitida para tener (es decir, elide) 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 proporciona la -fno-elide-constructors
para deshabilitar elision de copia. Si desea evitar una posible elision de copia, use -fno-elide-constructors
.
Ahora, casi todos los compiladores proporcionan elección de copia cuando la optimización está habilitada (y si no hay otra opción configurada para deshabilitarla).
Conclusión
Con cada elision de copia, se omiten una construcción y una destrucción de la copia correspondiente, lo que ahorra tiempo de CPU y no se crea un objeto, lo que ahorra espacio en el marco de la pila.