c++ - how - ¿Se permite llamar al destructor seguido explícitamente por una nueva ubicación en una variable con vida útil fija?
destroy object c++ (2)
Sé que llamar al destructor de forma explícita puede llevar a un comportamiento indefinido debido a la llamada del destructor doble, como aquí:
#include <vector>
int main() {
std::vector<int> foo(10);
foo.~vector<int>();
return 0; // Oops, destructor will be called again on return, double-free.
}
Pero, ¿y si llamamos ubicación nueva para "resucitar" el objeto?
#include <vector>
int main() {
std::vector<int> foo(10);
foo.~vector<int>();
new (&foo) std::vector<int>(5);
return 0;
}
Más formalmente:
- ¿Qué ocurrirá en C ++ (estoy interesado tanto en C ++ 03 como en C ++ 11, si hay una diferencia) si llamo explícitamente un destructor en algún objeto que no se construyó con una ubicación nueva en primer lugar (por ejemplo, es una variable local / global o se asignó con una
new
) y luego, antes de que se destruya este objeto, ¿es necesario colocar una nueva ubicación en él para "restaurarlo"? - Si está bien, ¿está garantizado que todas las referencias no constantes a ese objeto también estarán bien, siempre y cuando no las use mientras el objeto esté "muerto"?
- Si es así, ¿está bien usar una de las referencias no constantes para la ubicación nueva para resucitar el objeto?
- ¿Qué pasa con las referencias const?
Ejemplo de caso de uso (aunque esta pregunta es más acerca de la curiosidad): quiero "reasignar" un objeto que no tiene operator=
.
He visto this pregunta que dice que el objeto "de reemplazo" que tiene miembros const
no estáticos es ilegal. Entonces, limitemos el alcance de esta pregunta a los objetos que no tienen miembros const
.
Esto no es una buena idea, porque aún puede terminar ejecutando el destructor dos veces si el constructor del nuevo objeto lanza una excepción. Es decir, el destructor siempre se ejecutará al final del alcance, incluso si deja el alcance excepcionalmente.
Aquí hay un programa de ejemplo que muestra este comportamiento ( enlace Ideone ):
#include <iostream>
#include <stdexcept>
using namespace std;
struct Foo
{
Foo(bool should_throw) {
if(should_throw)
throw std::logic_error("Constructor failed");
cout << "Constructed at " << this << endl;
}
~Foo() {
cout << "Destroyed at " << this << endl;
}
};
void double_free_anyway()
{
Foo f(false);
f.~Foo();
// This constructor will throw, so the object is not considered constructed.
new (&f) Foo(true);
// The compiler re-destroys the old value at the end of the scope.
}
int main() {
try {
double_free_anyway();
} catch(std::logic_error& e) {
cout << "Error: " << e.what();
}
}
Esto imprime:
Construido en 0x7fff41ebf03f
Destruido a 0x7fff41ebf03f
Destruido a 0x7fff41ebf03f
Error: el constructor falló
Primero, [basic.life]/8
establece claramente que cualquier puntero o referencia al foo
original se referirá al nuevo objeto que construya en foo
en su caso. Además, el nombre foo
se referirá al nuevo objeto construido allí (también [basic.life]/8
).
En segundo lugar, debe asegurarse de que haya un objeto del tipo original que el almacenamiento usó para foo
antes de salir de su alcance; así que si algo lanza, debes atraparlo y terminar tu programa ( [basic.life]/9
).
En general, esta idea es a menudo tentadora, pero casi siempre es una idea horrible.
(8) Si, una vez que finaliza la vida útil de un objeto y antes de que se reutilice o libere el almacenamiento que ocupa el objeto ocupado, se crea un nuevo objeto en la ubicación de almacenamiento que ocupa el objeto original, un puntero que apunta al objeto original. una referencia que se refiera al objeto original, o el nombre del objeto original se referirá automáticamente al nuevo objeto y, una vez que la vida útil del nuevo objeto haya comenzado, se puede usar para manipular el nuevo objeto si:
- (8.1) el almacenamiento para el nuevo objeto superpone exactamente la ubicación de almacenamiento que ocupaba el objeto original, y
- (8.2) el nuevo objeto es del mismo tipo que el objeto original (ignorando los calificadores cv de nivel superior), y
- (8.3) el tipo del objeto original no está constantemente calificado y, si es un tipo de clase, no contiene ningún miembro de datos no estáticos cuyo tipo esté constantemente calificado o sea un tipo de referencia, y
- (8.4) el objeto original era el objeto más derivado (1.8) del tipo T y el nuevo objeto es el objeto más derivado del tipo T (es decir, no son subobjetos de la clase base).
(9) Si un programa finaliza la vida útil de un objeto de tipo T con estática (3.7.1), subproceso (3.7.2) o automática (3.7.3) duración del almacenamiento y si T tiene un destructor no trivial, el el programa debe garantizar que un objeto del tipo original ocupa la misma ubicación de almacenamiento cuando se realiza la llamada al destructor implícito; De lo contrario el comportamiento del programa es indefinido. Esto es cierto incluso si se sale del bloque con una excepción.
Hay razones para ejecutar destructores manualmente y hacer una nueva colocación. Algo tan simple como operator=
no es uno de ellos , a menos que esté escribiendo su propia variante / cualquier / vector o tipo similar.
Si realmente desea reasignar un objeto, busque una implementación std::optional
, y cree / destruya objetos con eso; es cuidadoso, y casi con seguridad no serás lo suficientemente cuidadoso.