Namespaces
Variants

Copy elision

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

Cuando se cumplen ciertos criterios, la creación de un objeto de clase a partir de un objeto fuente del mismo tipo (ignorando la calificación cv) puede omitirse, incluso si el constructor seleccionado y/o el destructor para el objeto tienen efectos secundarios. Esta omisión de la creación de objetos se denomina copy elision .

Contenidos

Explicación

La elisión de copia está permitida en las siguientes circunstancias (que pueden combinarse para eliminar múltiples copias):

  • En una return statement en una función con un tipo de retorno de clase, cuando el operando es el nombre de un objeto no volátil obj con automatic storage duration (que no sea un parámetro de función o un parámetro de handler ), la copy-initialization del objeto resultado puede omitirse construyendo obj directamente en el objeto resultado de la llamada a función. Esta variante de la eliminación de copia se conoce como named return value optimization (NRVO).
  • Cuando un objeto de clase target se inicializa por copia con un objeto de clase temporal obj que no ha sido vinculado a una referencia, la inicialización por copia puede omitirse construyendo obj directamente en target . Esta variante de la elisión de copia se conoce como optimización de valor de retorno sin nombre (URVO). Desde C++17, URVO es obligatoria y ya no se considera una forma de elisión de copia; ver más abajo.
(hasta C++17)
  • En una throw expresión , cuando el operando es el nombre de un objeto no volátil obj con duración de almacenamiento automático (que no sea un parámetro de función o un parámetro de manejador) que pertenece a un ámbito que no contiene el try bloque envolvente más interno (si existe), la inicialización por copia del objeto de excepción puede omitirse construyendo obj directamente en el objeto de excepción.
  • En un manejador , la inicialización por copia del argumento del manejador puede omitirse tratando el parámetro del manejador como un alias del objeto de excepción si el significado del programa permanece inalterado excepto por la ejecución de constructores y destructores para el argumento del manejador.
(desde C++11)
  • En corrutinas , puede omitirse una copia de un parámetro de corrutina. En este caso, las referencias a esa copia se reemplazan con referencias al parámetro correspondiente si el significado del programa permanece inalterado excepto por la ejecución de un constructor y destructor para el objeto copia del parámetro.
(desde C++20)

Cuando ocurre la elisión de copia, la implementación trata la fuente y el destino de la inicialización omitida simplemente como dos formas diferentes de referirse al mismo objeto.

La destrucción ocurre en el momento posterior entre los dos momentos en que los dos objetos habrían sido destruidos sin la optimización.

(until C++11)

Si el primer parámetro del constructor seleccionado es una referencia a valor del tipo del objeto, la destrucción de ese objeto ocurre cuando el objetivo habría sido destruido. De lo contrario, la destrucción ocurre en el momento posterior entre los dos momentos en que los dos objetos habrían sido destruidos sin la optimización.

(since C++11)


Semántica de prvalue ("eliminación de copia garantizada")

Desde C++17, un prvalue no se materializa hasta que se necesita, y luego se construye directamente en el almacenamiento de su destino final. Esto significa que incluso cuando la sintaxis del lenguaje sugiere visualmente una copia/movimiento (por ejemplo, inicialización por copia ), no se realiza ninguna copia/movimiento, lo que significa que el tipo no necesita tener un constructor de copia/movimiento accesible en absoluto. Los ejemplos incluyen:

T f()
{
    return U(); // constructs a temporary of type U,
                // then initializes the returned T from the temporary
}
T g()
{
    return T(); // constructs the returned T directly; no move
}
El destructor del tipo devuelto debe ser accesible en el punto de la sentencia return y no eliminado, incluso cuando no se destruya ningún objeto T.
  • En la inicialización de un objeto, cuando la expresión de inicialización es un prvalue del mismo tipo de clase (ignorando calificación cv ) que el tipo de la variable:
T x = T(T(f())); // x is initialized by the result of f() directly; no move
Esto solo puede aplicarse cuando se sabe que el objeto que se está inicializando no es un subobjeto potencialmente superpuesto:
struct C { /* ... */ };
C f();
struct D;
D g();
struct D : C
{
    D() : C(f()) {}    // no elision when initializing a base class subobject
    D(int) : D(g()) {} // no elision because the D object being initialized might
                       // be a base-class subobject of some other class
};

Nota: Esta regla no especifica una optimización, y el Estándar no la describe formalmente como "eliminación de copia" (porque no se está eliminando nada). En cambio, la especificación del lenguaje central de C++17 de prvalues y temporales es fundamentalmente diferente de la de revisiones anteriores de C++: ya no hay un temporal del cual copiar/mover. Otra forma de describir la mecánica de C++17 es "paso de valor no materializado" o "materialización temporal diferida": los prvalues se devuelven y se utilizan sin materializar nunca un temporal.

(desde C++17)

Notas

La elisión de copia es la única forma permitida de optimización (hasta C++14) una de las dos formas permitidas de optimización, junto con elisión y extensión de asignación , (desde C++14) que puede cambiar los efectos secundarios observables. Debido a que algunos compiladores no realizan la elisión de copia en todas las situaciones donde está permitida (por ejemplo, en modo depuración), los programas que dependen de los efectos secundarios de los constructores de copia/movimiento y destructores no son portables.

En una sentencia return o una expresión throw , si el compilador no puede realizar la copy elision pero se cumplen las condiciones para la copy elision, o se cumplirían excepto que la fuente es un parámetro de función, el compilador intentará usar el move constructor incluso si el operando fuente está designado por un lvalue (until C++23) el operando fuente será tratado como un rvalue (since C++23) ; consulte return statement para más detalles.

En constant expression y constant initialization , la copy elision nunca se realiza.

struct A
{
    void* p;
    constexpr A() : p(this) {}
    A(const A&); // Disable trivial copyability
};
constexpr A a;  // OK: a.p points to a
constexpr A f()
{
    A x;
    return x;
}
constexpr A b = f(); // error: b.p would be dangling and point to the x inside f
constexpr A c = A(); // (until C++17) error: c.p would be dangling and point to a temporary
                     // (since C++17) OK: c.p points to c; no temporary is involved
(since C++11)
Macro de prueba de características Valor Std Característica
__cpp_guaranteed_copy_elision 201606L (C++17) Eliminación de copia garantizada mediante categorías de valor simplificadas

Ejemplo

#include <iostream>
struct Noisy
{
    Noisy() { std::cout << "constructed at " << this << '\n'; }
    Noisy(const Noisy&) { std::cout << "copy-constructed\n"; }
    Noisy(Noisy&&) { std::cout << "move-constructed\n"; }
    ~Noisy() { std::cout << "destructed at " << this << '\n'; }
};
Noisy f()
{
    Noisy v = Noisy(); // (hasta C++17) elisión de copia inicializando v desde un temporal;
                       //               puede llamarse al constructor de movimiento
                       // (desde C++17) "elisión de copia garantizada"
    return v; // elisión de copia ("NRVO") desde v al objeto resultado;
              // puede llamarse al constructor de movimiento
}
void g(Noisy arg)
{
    std::cout << "&arg = " << &arg << '\n';
}
int main()
{
    Noisy v = f(); // (hasta C++17) elisión de copia inicializando v desde el resultado de f()
                   // (desde C++17) "elisión de copia garantizada"
    std::cout << "&v = " << &v << '\n';
    g(f()); // (hasta C++17) elisión de copia inicializando arg desde el resultado de f()
            // (desde C++17) "elisión de copia garantizada"
}

Salida posible:

constructed at 0x7fffd635fd4e
&v = 0x7fffd635fd4e
constructed at 0x7fffd635fd4f
&arg = 0x7fffd635fd4f
destructed at 0x7fffd635fd4f
destructed at 0x7fffd635fd4e

Informes de defectos

Los siguientes informes de defectos que modifican el comportamiento se aplicaron retroactivamente a los estándares publicados anteriormente de C++.

DR Aplicado a Comportamiento publicado Comportamiento correcto
CWG 1967 C++11 cuando se realiza la elisión de copia usando un constructor de movimiento,
aún se consideraba el tiempo de vida del objeto del que se movió
no se considera
CWG 2426 C++17 no se requería el destructor al retornar un prvalue el destructor se invoca potencialmente
CWG 2930 C++98 solo las operaciones de copia/movimiento podían ser elididas, pero un
constructor no-copia/movimiento puede ser seleccionado por inicialización-copia
elide cualquier construcción de objeto
de inicializaciones-copia relacionadas

Véase también