En la programación informática C++ , la elisión de copia se refiere a una técnica de optimización del compilador que elimina la copia innecesaria de objetos .
El estándar del lenguaje C++ generalmente permite que las implementaciones realicen cualquier optimización, siempre que el comportamiento observable del programa resultante sea el mismo que si , es decir, pretendiendo que el programa se ejecutara exactamente como lo exige el estándar. Más allá de eso, el estándar también describe algunas situaciones en las que se puede eliminar la copia incluso si esto alteraría el comportamiento del programa, siendo la más común la optimización del valor de retorno (ver más abajo). Otra optimización ampliamente implementada, descrita en el estándar C++ , es cuando un objeto temporal de tipo de clase se copia a un objeto del mismo tipo. [1] [2] Como resultado, la inicialización de copia suele ser equivalente a la inicialización directa en términos de rendimiento, pero no en semántica; la inicialización de copia todavía requiere un constructor de copia accesible . [3] La optimización no se puede aplicar a un objeto temporal que se ha vinculado a una referencia.
#include <flujo de datos> int n = 0 ; struct C { explicit C ( int ) {} C ( const C & ) { ++ n ; } // el constructor de copia tiene un efecto secundario visible }; // modifica un objeto con duración de almacenamiento estático int main () { C c1 ( 42 ); // inicialización directa, llama a C::C(int) C c2 = C ( 42 ); // inicialización de copia, llama a C::C(const C&) std :: cout << n << std :: endl ; // imprime 0 si se eliminó la copia, 1 en caso contrario }
Según el estándar, se puede aplicar una optimización similar a los objetos que se lanzan y se capturan , [4] [5] pero no está claro si la optimización se aplica tanto a la copia del objeto lanzado al objeto de excepción como a la copia del objeto de excepción al objeto declarado en la declaración de excepción de la cláusula catch . Tampoco está claro si esta optimización solo se aplica a los objetos temporales o también a los objetos nombrados. [6] Dado el siguiente código fuente:
#include <flujo de datos> struct C { C () = predeterminado ; C ( const C & ) { std :: cout << "¡Hola mundo! \n " ; } }; void f () { C c ; throw c ; // copiando el objeto nombrado c en el objeto de excepción. } // No está claro si esta copia puede elidirse (omitirse). int main () { try { f (); } catch ( C c ) { // copiando el objeto de excepción en el temporal en la // declaración de excepción. } // Tampoco está claro si esta copia puede elidirse (omitirse). }
Por lo tanto, un compilador conforme debería producir un programa que imprima "Hola mundo!" dos veces. En la revisión C++11 del estándar C++, se han abordado los problemas, permitiendo esencialmente que se omita tanto la copia del objeto nombrado al objeto de excepción como la copia al objeto declarado en el manejador de excepciones. [6]
GCC ofrece la -fno-elide-constructors
opción de desactivar la eliminación de copias. Esta opción es útil para observar (o no) los efectos de la optimización del valor de retorno u otras optimizaciones en las que se eliminan copias. Por lo general, no se recomienda desactivar esta importante optimización.
C++17 proporciona "elisión de copia garantizada", un prvalue no se materializa hasta que se necesita y luego se construye directamente en el almacenamiento de su destino final. [7]
En el contexto del lenguaje de programación C++ , la optimización del valor de retorno ( RVO ) es una optimización del compilador que implica eliminar el objeto temporal creado para contener el valor de retorno de una función . [8] El estándar C++ permite que RVO cambie el comportamiento observable del programa resultante . [9]
En general, el estándar C++ permite que un compilador realice cualquier optimización, siempre que el ejecutable resultante muestre el mismo comportamiento observable como si (es decir, pretendiendo) se hubieran cumplido todos los requisitos del estándar. Esto se conoce comúnmente como la " regla como si ". [10] [2] El término optimización del valor de retorno se refiere a una cláusula especial en el estándar C++ que va incluso más allá de la regla "como si": una implementación puede omitir una operación de copia resultante de una declaración de retorno , incluso si el constructor de copia tiene efectos secundarios . [1] [2]
El siguiente ejemplo demuestra un escenario en el que la implementación puede eliminar una o ambas copias que se están realizando, incluso si el constructor de copias tiene un efecto secundario visible (imprimir texto). [1] [2] La primera copia que se puede eliminar es aquella en la que C
se puede copiar un temporal sin nombre en el valor de retornof
de la función . La segunda copia que se puede eliminar es la copia del objeto temporal devuelto por to .f
obj
#include <flujo de datos> struct C { C () = default ; C ( const C & ) { std :: cout << "Se realizó una copia. \n " ; } }; C f () { devolver C (); } int main () { std :: cout << "¡Hola mundo! \n " ; C obj = f (); }
Dependiendo del compilador y de la configuración de éste, el programa resultante puede mostrar cualquiera de las siguientes salidas:
¡Hola Mundo!Se hizo una copia.Se hizo una copia.
¡Hola Mundo!Se hizo una copia.
¡Hola Mundo!
Devolver un objeto de tipo incorporado desde una función normalmente conlleva poca o ninguna sobrecarga, ya que el objeto normalmente cabe en un registro de CPU . Devolver un objeto más grande de tipo de clase puede requerir una copia más costosa de una ubicación de memoria a otra. Para evitar esto, una implementación puede crear un objeto oculto en el marco de pila del llamador y pasar la dirección de este objeto a la función. El valor de retorno de la función se copia luego en el objeto oculto. [11] Por lo tanto, código como este:
estructura Datos { char bytes [ 16 ]; }; Datos F () { Datos resultado = {}; // generar resultado devolver resultado ; } int main () { Datos d = F (); }
Puede generar un código equivalente a este:
estructura Datos { char bytes [ 16 ]; }; Datos * F ( Datos * _hiddenAddress ) { Datos resultado = {}; // copiar el resultado en el objeto oculto * _hiddenAddress = resultado ; devolver _hiddenAddress ; } int main () { Data _hidden ; // crear objeto oculto Data d = * F ( & _hidden ); // copiar el resultado en d }
lo que hace que el Data
objeto se copie dos veces.
En las primeras etapas de la evolución de C++ , la incapacidad del lenguaje para devolver eficientemente un objeto de tipo clase desde una función se consideraba una debilidad. [12] Alrededor de 1991, Walter Bright implementó una técnica para minimizar la copia, reemplazando efectivamente el objeto oculto y el objeto nombrado dentro de la función con el objeto utilizado para almacenar el resultado: [13]
estructura Datos { char bytes [ 16 ]; }; void F ( Data * p ) { // generar resultado directamente en *p } int main () { Datos d ; F ( & d ); }
Bright implementó esta optimización en su compilador Zortech C++ . [12] Esta técnica particular fue posteriormente denominada "Optimización del valor de retorno nombrado" (NRVO), en referencia al hecho de que se omite la copia de un objeto nombrado. [13]
La mayoría de los compiladores admiten la optimización del valor de retorno. [8] [14] [15] Sin embargo, pueden existir circunstancias en las que el compilador no pueda realizar la optimización. Un caso común es cuando una función puede devolver objetos con nombres diferentes según la ruta de ejecución: [11] [14] [16]
#include <string> std :: string F ( bool cond = false ) { std :: string first ( "first" ); std :: string second ( "second" ); // la función puede devolver uno de dos objetos nombrados // dependiendo de su argumento. Es posible que no se aplique RVO return cond ? first : second ; } int main () { std :: string resultado = F (); }