¿Cómo funciona la elisión de copia garantizada?

8 minutos de lectura

¿Como funciona la elision de copia garantizada
jotik

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?

1647572650 491 ¿Como funciona la elision de copia garantizada
Nicolás Bolas

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 rtjunto 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).

  • @BenVoigt: Poner tipos definidos por el usuario no copiables de forma trivial en registros no es algo viable que pueda hacer una ABI, ya sea que la elisión esté disponible o no.

    – Nicolás Bolas

    26 de junio de 2016 a las 22:49

  • Ahora que las reglas son públicas, puede valer la pena actualizar esto con el concepto “prvalues ​​are initializations”.

    – Johannes Schaub – litb

    12/09/2016 a las 19:36

  • @JohannesSchaub-litb: solo es “ambiguo” si sabe demasiado sobre las minucias del estándar C ++. Para el 99 % de la comunidad de C++, sabemos a qué se refiere la “eliminación de copia garantizada”. El documento real que propone la función es incluso noble “Elisión de copia garantizada”. Agregar “a través de categorías de valor simplificadas” simplemente hace que sea confuso y difícil de entender para los usuarios. También es un nombre inapropiado, ya que estas reglas en realidad no “simplifican” las reglas en torno a las categorías de valor. Te guste o no, el término “eliminación de copia garantizada” se refiere a esta función y nada más.

    – Nicolás Bolas

    28 de mayo de 2017 a las 13:41

  • Tengo tantas ganas de poder recoger un prvalue y llevarlo consigo. Supongo que esto es solo un (one-shot) std::function<T()> De Verdad.

    – Yakk – Adam Nevraumont

    30 de mayo de 2017 a las 17:17

  • @LukasSalich: Esa es una pregunta de C++ 11. Esta respuesta es sobre una característica de C++17.

    – Nicolás Bolas

    26 de junio de 2019 a las 19:04

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. .

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad