La Copy Elision es una optimización que evita crear copias intermedias construyendo el objeto directamente en su destino final.
Hemos recorrido un largo camino optimizando nuestro código C++.
- Aprendimos que la Copia es lenta (clona todo el objeto).
- Aprendimos que el Movimiento (
std::move) es rápido (roba los punteros).
Parece que mover es la velocidad máxima a la que podemos aspirar, ¿verdad? Pues no. Hay algo todavía más rápido que mover: No hacer nada.
Hoy hablamos de la Copy Elision (Elisión de Copia).
Es una técnica (y desde C++17, una garantía) mediante la cual el compilador es tan inteligente que se da cuenta de que va a crear un objeto temporal solo para copiarlo/moverlo a otro sitio… y decide saltarse el paso intermedio.
El compilador construye el objeto directamente en su destino final.
El experimento: un objeto “ruidoso”
Para ver esto en acción, necesitamos una clase que nos grite cada vez que la tocamos. Vamos a desactivar la optimización mentalmente un segundo y ver qué debería pasar según la teoría clásica.
#include <iostream>
struct Ruidosa {
Ruidosa() { std::cout << "Constructor\n"; }
~Ruidosa() { std::cout << "Destructor\n"; }
// Constructor de Copia
Ruidosa(const Ruidosa&) { std::cout << "Copia\n"; }
// Constructor de Movimiento
Ruidosa(Ruidosa&&) { std::cout << "Movimiento\n"; }
};
Ruidosa crearObjeto() {
return Ruidosa(); // Crea un temporal y lo devuelve
}
int main() {
std::cout << "--- Inicio ---\n";
Ruidosa miObjeto = crearObjeto();
std::cout << "--- Fin ---\n";
}
Lo que esperaríamos sin copy elision
Si seguimos la lógica estricta paso a paso:
crearObjetollama al Constructor para crear el temporal.- El
returnMueve (o Copia) ese temporal haciamiObjetoen elmain. - Se destruye el temporal de la función.
- Al final del main, se destruye
miObjeto.
Esperaríamos ver: Constructor -> Movimiento -> Destructor -> Destructor.
Lo que ocurre con copy elision
Si compilas ese código (incluso sin flags de optimización en compiladores modernos), verás esto:
--- Inicio ---
Constructor
--- Fin ---
Destructor
¡No hay copia! ¡No hay movimiento!
El compilador ha visto que miObjeto es el destino final del objeto creado dentro de la función. Así que, en lugar de crearlo en la función y moverlo, le pasa la dirección de memoria de miObjeto a la función y lo construye directamente allí.
RVO y NRVO
Esta magia tiene nombres técnicos.
RVO (Return Value Optimization)
Ocurre cuando devolvemos un temporal sin nombre (un prvalue).
return Ruidosa(); // RVO
Desde C++17, esto no es una optimización opcional. Es obligatoria. Se conoce como Guaranteed Copy Elision. Significa que incluso si tu clase tiene el constructor de copia/movimiento borrado (delete), ¡el código compilará igual! Porque técnicamente no se está llamando a ninguno.
NRVO (Named Return Value Optimization)
Ocurre cuando devolvemos una variable local que tiene nombre.
Ruidosa crear() {
Ruidosa local; // Variable con nombre
// ... hacemos cosas con local ...
return local; // NRVO
}
Aquí el compilador intenta alinear la variable local con el hueco de memoria donde espera el resultado la función llamante. Esto sigue siendo una optimización (no es obligatorio por el estándar), pero todos los compiladores decentes lo hacen.
El antipatrón de std::move en return
Un error habitual al aprender std::move es intentar forzar el movimiento al devolver una variable local.
Piensan: “Como voy a devolver una variable local y quiero que sea eficiente, voy a forzar el movimiento”.
// ❌ MAL: Rompes la NRVO
Ruidosa funcionMala() {
Ruidosa local;
return std::move(local); // ¡ERROR!
}
¿Por qué está mal?
Al hacer std::move(local), conviertes la expresión en un X-value y deja de cumplir la forma necesaria para NRVO. Normalmente, esto provoca una llamada al constructor de movimiento.
Al forzar el movimiento, impides que el compilador haga Copy Elision.
- Con
std::move: Llamada al constructor + Llamada al Move Constructor. - Sin
std::move: Solo llamada al constructor (construcción directa).
Idea práctica: no uses std::move en una sentencia return cuando devuelvas una variable local. El compilador ya sabe que se va a destruir y la optimizará mejor que tú.
Efectos secundarios
La Copy Elision tiene una peculiaridad: cambia el comportamiento observable del programa.
Normalmente, las optimizaciones (como loop unrolling) hacen que el programa vaya más rápido pero haga exactamente lo mismo. La Copy Elision elimina llamadas a constructores y destructores.
Si tu constructor de copia tenía un “efecto secundario” (como incrementar un contador global, imprimir por pantalla, o abrir una conexión), ese efecto no ocurrirá.
Por eso, en C++, los constructores de copia/movimiento no deberían tener efectos secundarios críticos en la lógica del programa, porque no puedes fiarte al 100% de cuántas veces se llamarán.