En la reunión de estándares ISO C++ de Oulu de 2016, se presentó una propuesta denominada Elisión de copia garantizada a través de categorías de valor simplificadas fue votado en C++17 por el comité de estándares.
¿Cómo funciona exactamente la elisión de copia garantizada? ¿Cubre algunos casos en los que ya se permitía la elisión de copias o se necesitan cambios en el código para garantizar la elisión de copias?
Se permitió que ocurriera la elisión de copias bajo una serie de circunstancias. Sin embargo, incluso si estuviera permitido, el código aún tenía que poder funcionar como si la copia no estuviera elidida. Es decir, tenía que haber un constructor de copia y/o movimiento accesible.
La elisión de copia garantizada redefine una serie de conceptos de C++, de modo que ciertas circunstancias en las que se pueden elidir copias/movimientos en realidad no provocan una copia/movimiento en absoluto. El compilador no elimina una copia; el estándar dice que tal copia nunca podría ocurrir.
Considere esta función:
T Func() {return T();}
Bajo las reglas de elisión de copia no garantizada, esto creará un temporal, luego pasará de ese temporal al valor de retorno de la función. Esa operación de movimiento mayo ser elidido, pero T
aún debe tener un constructor de movimiento accesible incluso si nunca se usa.
Similar:
T t = Func();
Esta es la inicialización de copia de t
. Esto copiará la inicialización t
con el valor de retorno de Func
. Sin embargo, T
todavía tiene que tener un constructor de movimiento, aunque no se llamará.
Elisión de copia garantizada redefine el significado de una expresión prvalue. Pre-C++17, los prvalues son objetos temporales. En C++17, una expresión prvalue es simplemente algo que puede materializar un temporal, pero no es un temporal todavía.
Si usa un prvalue para inicializar un objeto del tipo prvalue, entonces no se materializa ningún temporal. Cuando tu lo hagas return T();
, esto inicializa el valor de retorno de la función a través de un prvalue. Como esa función devuelve T
, no se crea ningún temporal; la inicialización del prvalue simplemente inicia directamente el valor de retorno.
Lo que hay que entender es que, dado que el valor devuelto es un prvalue, es no es un objeto aún. Es simplemente un inicializador para un objeto, al igual que T()
es.
Cuando tu lo hagas T t = Func();
el prvalue del valor de retorno inicializa directamente el objeto t
; no hay una etapa de “crear un temporal y copiar/mover”. Ya que Func()
El valor de retorno de es un prvalue equivalente a T()
, t
es inicializado directamente por T()
exactamente como si lo hubieras hecho T t = T()
.
Si se usa un prvalue de cualquier otra forma, el prvalue materializará un objeto temporal, que se usará en esa expresión (o se descartará si no hay expresión). entonces si lo hiciste const T &rt = Func();
el prvalue materializaría un temporal (usando T()
como inicializador), cuya referencia se almacenaría en rt
junto con las habituales extensiones temporales de por vida.
Una cosa que la elisión garantizada te permite hacer es devolver objetos que están inmóviles. Por ejemplo, lock_guard
no se puede copiar ni mover, por lo que no podría tener una función que lo devolviera por valor. Pero con la elisión de copia garantizada, puede hacerlo.
La elisión garantizada también funciona con la inicialización directa:
new T(FactoryFunction());
Si FactoryFunction
devoluciones T
por valor, esta expresión no copiará el valor de retorno en la memoria asignada. En su lugar, asignará memoria y usará la memoria asignada como la memoria de valor de retorno para la llamada de función directamente.
Entonces, las funciones de fábrica que regresan por valor pueden inicializar directamente la memoria asignada en montón sin siquiera saberlo. Mientras estos funcionen internamente siga las reglas de elisión de copia garantizada, por supuesto. Tienen que devolver un prvalue de tipo T
.
Por supuesto, esto también funciona:
new auto(FactoryFunction());
En caso de que no te guste escribir typenames.
Es importante reconocer que las garantías anteriores sólo funcionan por valor de prvalues. Es decir, no obtiene ninguna garantía al devolver un llamado variable:
T Func()
{
T t = ...;
...
return t;
}
En este caso, t
aún debe tener un constructor de copiar/mover accesible. Sí, el compilador puede optar por optimizar la copia/mover. Pero el compilador aún debe verificar la existencia de un constructor de copiar/mover accesible.
Así que nada cambia para la optimización del valor de retorno con nombre (NRVO).
Creo que aquí se han compartido bien los detalles de la elisión de copias. Sin embargo, encontré este artículo: https://jonasdevlieghere.com/guaranteed-copy-elision que se refiere a la elisión de copia garantizada en C++ 17 en el caso de optimización del valor de retorno.
También se refiere a cómo usar la opción gcc: -fno-elide-constructors, uno puede deshabilitar la elisión de copia y ver que en lugar de llamar directamente al constructor en el destino, vemos 2 constructores de copia (o mover constructores en c ++ 11 ) y sus correspondientes destructores siendo llamados. El siguiente ejemplo muestra ambos casos:
#include <iostream>
using namespace std;
class Foo {
public:
Foo() {cout << "Foo constructed" << endl; }
Foo(const Foo& foo) {cout << "Foo copy constructed" << endl;}
Foo(const Foo&& foo) {cout << "Foo move constructed" << endl;}
~Foo() {cout << "Foo destructed" << endl;}
};
Foo fReturnValueOptimization() {
cout << "Running: fReturnValueOptimization" << endl;
return Foo();
}
Foo fNamedReturnValueOptimization() {
cout << "Running: fNamedReturnValueOptimization" << endl;
Foo foo;
return foo;
}
int main() {
Foo foo1 = fReturnValueOptimization();
Foo foo2 = fNamedReturnValueOptimization();
}
vinegupt@bhoscl88-04(~/progs/cc/src)$ g++ -std=c++11 testFooCopyElision.cxx # Copy elision enabled by default
vinegupt@bhoscl88-04(~/progs/cc/src)$ ./a.out
Running: fReturnValueOptimization
Foo constructed
Running: fNamedReturnValueOptimization
Foo constructed
Foo destructed
Foo destructed
vinegupt@bhoscl88-04(~/progs/cc/src)$ g++ -std=c++11 -fno-elide-constructors testFooCopyElision.cxx # Copy elision disabled
vinegupt@bhoscl88-04(~/progs/cc/src)$ ./a.out
Running: fReturnValueOptimization
Foo constructed
Foo move constructed
Foo destructed
Foo move constructed
Foo destructed
Running: fNamedReturnValueOptimization
Foo constructed
Foo move constructed
Foo destructed
Foo move constructed
Foo destructed
Foo destructed
Foo destructed
Veo que la optimización del valor de retorno, es decir, la elisión de copia de objetos temporales en las declaraciones de retorno, generalmente se garantiza independientemente de c ++ 17.
Sin embargo, la optimización del valor de retorno con nombre de las variables locales devueltas ocurre principalmente pero no está garantizada. En una función con diferentes declaraciones de retorno, veo que si cada una de las declaraciones de retorno devuelve variables de alcance local o variables del mismo alcance, sucederá. De lo contrario, si en diferentes declaraciones de devolución se devuelven variables de diferentes ámbitos, sería difícil para el compilador realizar la elisión de copia.
Sería bueno, si hubiera una manera de garantizar la elisión de copia o recibir algún tipo de advertencia cuando no se puede realizar la elisión de copia, lo que haría que los desarrolladores se aseguren de realizar la elisión de copia y refactorizar el código si no se puede realizar. .