Tiempo de vida
Cada objeto y referencia tiene un tiempo de vida, que es una propiedad en tiempo de ejecución: para cada objeto o referencia, hay un punto de la ejecución del programa donde comienza su tiempo de vida, y un momento en que termina.
- Para cualquier objeto de clase o tipos agregados si este, o cualquiera de sus subobjetos, es inicializado por algo que no sea el constructor trivial por defecto, el tiempo de vida comienza cuando finaliza la inicialización.
- Para cualquier objeto de tipo clase cuyo destructor no sea el predeterminado, el tiempo de vida finaliza cuando comienza la ejecución del destructor.
- El tiempo de vida de un miembro de una union comienza cuando ese miembro se hace activo.
- Para los demás objetos (objetos de clase incializados por el constructor por defecto, objetos no clase, arrays de estos, etc.), el tiempo de vida comienza cuando se reserva, correctamente alineado, el almacenamiento para el objeto, y termina cuando el almacenamiento se libera o se reutiliza para otro objeto.
El tiempo de vida de un objeto esta asociado con el tiempo de vida de su almacenamiento, ver duración del almacenamiento.
(hasta C++14) El tiempo de vida de una referencia es exactamente la duración de su almacenamiento.
(desde C++14) El tiempo de vida de una referencia comienza cuando se completa la inicialización y termina como si fuera un objeto escalar.
Nota: la vida útil de un objeto referenciado puede finalizar antes del final de la vida útil de la referencia, lo que posibilita referencias colgadas.
El tiempo de vida de los miembros de objeto y subojetos de base comienza y termina siguiendo el orden de inicialización de clase.
Contenido |
[editar] Tiempo de vida de objetos temporales
Los objetos temporales se crean cuando un prvalue se materializa para que se pueda usar como un glvalue (desde C++17), que ocurre en las siguientes situaciones:
|
(hasta C++17) |
|
(desde C++17) |
La materialización de un objeto temporal generalmente se retrasa el mayor tiempo posible para evitar crear objetos temporales innecesarios: ver copia elision. |
(desde C++17) |
Todos los objetos temporales se destruyen como último paso en la evaluación de la expresión completa que (léxicamente) contiene el punto donde se crearon, y si se crearon múltiples objetos temporales, se destruyen en el orden opuesto al orden de creación. Esto es cierto incluso si esa evaluación termina lanzando una excepción.
Hay dos excepciones:
- El tiempo de vida de un objeto temporal se puede ampliar vinculándolo a una referencia constante lValue o a una referencia rvalue (desde C++11), para más detalles ver inicialización de referencia.
|
(desde C++11) |
[editar] Reutilización de almacenamiento
Un programa no necesita llamar al destructor de clase para finalizar el tiempo de vida si el objeto tiene un destructor por defecto o si no afecta al programa los efectos del destructor. Sin embargo, si un programa finaliza la vida útil de un objeto no trivial explícitamente, debe garantizar que un nuevo objeto del mismo tipo se construya in situ (por ejemplo, mediante el operador new) antes de que el destructor pueda invocarse implícitamente, es decir, debido a la salida del ámbito o una excepción para objetos automáticos, debido a la terminación de hilos para objetos locales al hilo o debido a la salida de programas para objetos estáticos; de lo contrario, el comportamiento es indeterminado.
class T {}; // trivial (destructor por defecto) struct B { ~B() {} // no trivial (se define destructor) }; void x() { long long n; // automático, trivial new (&n) double(3.14); // se reutiliza con un tipo diferente } // correcto void h() { B b; // automático, no destructible trivialmente b.~B(); // fin tiempo de vida (no necesario, puesto que no afecta a la ejecución) new (&b) T; // tipo erróneo: correcto hasta que se llama al destructor } // se llama al destructor: comportamiento indeterminado.
El comportamiento es indeterminado para la reutilización de almacenamiento que está o estuvo ocupado por un objeto constante completo de almacenamiento estático, local al hilo o automático porque esos objetos pueden estar almacenados en memoria de solo lectura.
struct B { B(); // no trivial ~B(); // no trivial }; const B b; // estático constante void h() { b.~B(); // fin del tiempo de vida de b new (const_cast<B*>(&b)) const B; // comportamiento indeterminado: // se reutiliza el espacio de una constante }
Si se crea un objeto nuevo en la dirección ocupada por otro objeto, entonces todos los punteros, referencias, y nombres del objeto original automáticamente se referirán al objeto nuevo y, una vez que comienza la vida del nuevo objeto, se pueden utilizar para manipular el nuevo objeto, pero solamente si se cumplen las siguientes condiciones:
- el almacenamiento para el nuevo objeto coincide exactamente con el espacio que ocupaba el original.
- el nuevo objeto es del mismo tipo que el original (ignorando los calificadores de alto nivel).
- el tipo del objeto original no está calificado como constante.
- si el objeto original tenía un tipo de clase, no contiene ningún miembro de datos no estáticos cuyo tipo es calificado constante o un tipo de referencia.
- el objeto original era el objeto más derivado del tipo T y el nuevo objeto es el más derivado del tipo T (es decir, no son subobjetos de clase base).
struct C { int i; void f(); const C& operator=( const C& ); }; const C& C::operator=( const C& other) { if ( this != &other ) { this->~C(); // termina tiempo de vida de *this new (this) C(other); // se crea nuevo objeto de tipo C f(); // bien definido } return *this; } C c1; C c2; c1 = c2; // bien definido c1.f(); // bien definido; c1 se refiere al nuevo objeto de tipo C
Si las condiciones anteriores no se cumplen, todavía se puede obtener un puntero válido al nuevo objeto aplicando la barrera de optimización del puntero std::launder. Igualmente, si un objeto se crea en el almacenamiento de un miembro de clase o elemento de array, el objeto creado es solamente un subobjeto (miembro o elemento) del objeto que contiene el objeto original si:
De lo contrario (si el subobjeto contiene un miembro de referencia o un subobjeto constante), el nombre del subobjeto original no se puede usar para acceder al nuevo objeto sin std::launder: struct X { const int n; }; union U { X x; float f; }; void tong() { U u = { { 1 } }; u.f = 5.f; // OK, crear nuevo subojeto de 'u' X *p = new (&u.x) X {2}; // OK, crear nuevo subobjeto de 'u' assert(p->n == 2); // OK assert(*std::launder(&u.x.n) == 2); // OK assert(u.x.n == 2); // indterminado: 'u.x' no nombra al nuevo subobjeto } Como caso especial, se pueden crear objetos en arrays de
Si esa parte del array proporcionó previamente almacenamiento para otro objeto, el tiempo de vida de ese objeto finaliza porque se reutilizó su almacenamiento; sin embargo, la vida útil del array no termina (no se considera que su almacenamiento se haya reutilizado). template<typename ...T> struct AlignedUnion { alignas(T...) unsigned char data[max(sizeof(T)...)]; }; int f() { AlignedUnion<int, char> au; int *p = new (au.data) int; // de acuerdo, au.data proporciona espacio char *c = new (au.data) char(); // de acuerdo, finaliza tiempo vida de *p char *d = new (au.data + 1) char(); return *c + *d; // de acuerdo } |
(desde C++17) |
[editar] Acceso fuera del tiempo de vida
Antes de que comience el tiempo de vida de un objeto, pero después de que se haya asignado el espacio que ocupará el objeto o, una vez que haya finalizado el tiempo de vida de un objeto y antes de que se reutilice o libere el almacenamiento, los siguientes usos de la expresión glvalue que identifica ese objeto es indeterminado:
- Conversión de lvalue to rvalue (por ejemplo: Llamada de función a una función que toma un valor)
- Acceso a un miembro de datos no estático o una llamada a una función miembro no estática.
- Enlace a una referencia a un subobjeto de la clase base virtual.
- expresiones dynamic_cast o typeid.
Las reglas anteriores también se aplican a los punteros (al vincular una referencia a una base virtual se reemplaza por una conversión implícita a un puntero a la base virtual), con dos reglas adicionales:
- static_cast de un puntero al almacenamiento sin un objeto solo se permite cuando se realiza un casting (posiblemente calificado) void*.
- Los punteros al almacenamiento sin un objeto al que se realizó cast probablemente calificado a void* solamente pueden realizar static_cast a punteros probablemente calificados a char, unsigned char o std::byte.
Durante la construcción y la destrucción, se aplican otras restricciones, ver llamadas a función virtual durante la construcción y la destrucción.
[editar] Notas
La diferencia entre las reglas de fin de tiempo de vida de objetos no de clase (fin de la duración del almacenamiento) y los objetos de clase (orden inverso de construcción) se puede ver en el siguiente ejemplo:
struct A { int* p; ~A() { std::cout << *p; } // si 'n' dura más tiempo que 'a', imprime 123 }; void f() { A a; int n = 123; // si 'n' no dura más que 'a', está optimizado (espacio muerto) a.p = &n; }
[editar] Véase también
Documentación de C para Tiempo de vida
|