¿Llamar a destructor manualmente siempre es un signo de mal diseño?

Estaba pensando: dicen que si llamas a destructor de forma manual, estás haciendo algo mal. Pero es siempre el caso? ¿Hay algún contraejemplo? ¿Situaciones donde es necesario llamarlo manualmente o donde es difícil / imposible / poco práctico evitarlo?

Llamar al destructor manualmente es necesario si el objeto se construyó utilizando una forma sobrecargada del operator new() , excepto cuando se utilizan las sobrecargas ” std::nothrow “:

 T* t0 = new(std::nothrow) T(); delete t0; // OK: std::nothrow overload void* buffer = malloc(sizeof(T)); T* t1 = new(buffer) T(); t1->~T(); // required: delete t1 would be wrong free(buffer); 

Sin embargo, la administración externa de la memoria en un nivel bastante bajo como el que se llama a los destructores explícitamente, es un signo de mal diseño. Probablemente, en realidad no es solo un mal diseño, sino un error absoluto (sí, usar un destructor explícito seguido de una llamada de constructor de copia en el operador de asignación es un mal diseño y es probable que esté equivocado).

Con C ++ 2011 hay otra razón para usar llamadas a destructor explícitas: cuando se usan uniones generalizadas, es necesario destruir explícitamente el objeto actual y crear un nuevo objeto utilizando la colocación nueva al cambiar el tipo del objeto representado. Además, cuando se destruye la unión, es necesario llamar explícitamente al destructor del objeto actual si requiere destrucción.

Todas las respuestas describen casos específicos, pero hay una respuesta general:

Llame al dtor explícitamente cada vez que necesite destruir el objeto (en sentido C ++) sin liberar la memoria en la que reside el objeto.

Esto suele ocurrir en todas las situaciones en las que la asignación / desasignación de memoria se gestiona independientemente de la construcción / destrucción de objetos. En esos casos, la construcción ocurre a través de la colocación nueva en un trozo de memoria existente, y la destrucción ocurre a través de una llamada explícita.

Aquí está el ejemplo crudo:

 { char buffer[sizeof(MyClass)]; { MyClass* p = new(buffer)MyClass; p->dosomething(); p->~MyClass(); } { MyClass* p = new(buffer)MyClass; p->dosomething(); p->~MyClass(); } } 

Otro ejemplo notable es el predeterminado std::allocator cuando se usa std::vector : los elementos se construyen en vector durante push_back , pero la memoria se asigna en fragmentos, por lo que preexiste la construcción del elemento. Y por lo tanto, vector::erase debe destruir los elementos, pero no necesariamente desasigna la memoria (especialmente si los nuevos push_back tienen que suceder pronto …).

Es “mal diseño” en estricto sentido de OOP (debe administrar objetos, no memoria: el hecho de que los objetos requieren memoria es un “incidente”), es “buen diseño” en “progtwigción de bajo nivel”, o en casos donde la memoria es no tomado de la “tienda gratuita”, el operator new predeterminado operator new compra.

Es un mal diseño si ocurre aleatoriamente alrededor del código, es un buen diseño si ocurre localmente a clases específicamente diseñadas para ese propósito.

No, depende de la situación, a veces es un diseño legítimo y bueno .

Para entender por qué y cuándo necesita llamar a los destructores explícitamente, veamos qué ocurre con “nuevo” y “eliminar”.

Para crear un objeto dinámicamente, T* t = new T; bajo el capó: 1. Se asigna la memoria sizeof (T). 2. El constructor de T se llama para inicializar la memoria asignada. El operador nuevo hace dos cosas: asignación e inicialización.

Para destruir el objeto, delete t; debajo del capó: 1. Se llama al destructor de T. 2. Se libera la memoria asignada para ese objeto. la eliminación del operador también hace dos cosas: destrucción y desasignación.

Uno escribe el constructor para hacer la inicialización y el destructor para hacer la destrucción. Cuando llama explícitamente al destructor, solo se realiza la destrucción, pero no la desasignación .

Por lo tanto, un uso legítimo de un destructor de invocación explícita podría ser “Solo deseo destruir el objeto, pero no (o no) liberar la asignación de memoria (todavía)”.

Un ejemplo común de esto es la memoria preasignada para un grupo de ciertos objetos que, de lo contrario, deben asignarse dinámicamente.

Al crear un nuevo objeto, obtiene la porción de memoria del grupo preasignado y realiza una “ubicación nueva”. Después de terminar con el objeto, es posible que desee llamar explícitamente al destructor para finalizar el trabajo de limpieza, si corresponde. Pero en realidad no desasignará la memoria, como hubiera hecho la eliminación del operador. En cambio, devuelve el trozo a la piscina para su reutilización.

Como se cita en las Preguntas frecuentes, debe llamar al destructor de forma explícita cuando use la ubicación nueva .

Esta es la única vez que llamas explícitamente un destructor.

Aunque estoy de acuerdo en que esto rara vez es necesario.

No, no debes llamarlo explícitamente porque se llamaría dos veces. Una vez para la llamada manual y otra vez cuando el scope en el que se declara el objeto finaliza.

P.ej.

 { Class c; c.~Class(); } 

Si realmente necesita realizar las mismas operaciones, debe tener un método diferente.

Hay una situación específica en la que es posible que desee llamar a un destructor en un objeto asignado dinámicamente con una ubicación new pero no suena algo que alguna vez necesitará.

Cada vez que necesite separar la asignación de la inicialización, necesitará colocar llamadas nuevas y explícitas del destructor manualmente. Hoy en día, rara vez es necesario, ya que tenemos los contenedores estándar, pero si tiene que implementar algún tipo de contenedor nuevo, lo necesitará.

Hay casos en que son necesarios:

En el código en el que trabajo utilizo llamadas de destructor explícitas en los asignativos, tengo la implementación del asignador simple que usa la ubicación nueva para devolver los bloques de memoria a los contenedores stl. En destruir tengo:

  void destroy (pointer p) { // destroy objects by calling their destructor p->~T(); } 

mientras está en construcción:

  void construct (pointer p, const T& value) { // initialize memory with placement new #undef new ::new((PVOID)p) T(value); } 

también se realiza una asignación en allocate () y desasignación de memoria en deallocate (), utilizando los mecanismos alloc y dealloc específicos de la plataforma. Este asignador se usó para evitar el uso de doug lea malloc y usarlo directamente, por ejemplo, LocalAlloc en Windows.

¿Qué hay de esto?
Destructor no se llama si se lanza una excepción desde el constructor, por lo que tengo que llamarlo manualmente para destruir los identificadores que se han creado en el constructor antes de la excepción.

 class MyClass { HANDLE h1,h2; public: MyClass() { // handles have to be created first h1=SomeAPIToCreateA(); h2=SomeAPIToCreateB(); ... try { if(error) { throw MyException(); } } catch(...) { this->~MyClass(); throw; } } ~MyClass() { SomeAPIToDestroyA(h1); SomeAPIToDestroyB(h2); } }; 

Nunca me encontré con una situación en la que uno necesita llamar a un destructor manualmente. Me parece recordar que incluso Stroustrup afirma que es una mala práctica.

Encontré 3 ocasiones en las que necesitaba hacer esto:

  • Asignación / desasignación de objetos en la memoria creada por memory-mapped-io o memoria compartida
  • cuando implementemos una interfaz C dada usando C ++ (sí, esto todavía sucede hoy lamentablemente (porque no tengo suficiente influencia para cambiarlo))
  • cuando implementa clases de asignador

Encontré otro ejemplo donde tendría que llamar destructor (es) manualmente. Supongamos que ha implementado una clase similar a una variante que contiene uno de varios tipos de datos:

 struct Variant { union { std::string str; int num; bool b; }; enum Type { Str, Int, Bool } type; }; 

Si la instancia de Variant tenía una std::string , y ahora está asignando un tipo diferente a la unión, primero debe destruir std::string . El comstackdor no hará eso automáticamente .

La memoria no es diferente a otros recursos: debería echarle un vistazo a http://channel9.msdn.com/Events/GoingNative/GoingNative-2012/Keynote-Bjarne-Stroustrup-Cpp11-Style, especialmente la parte donde Bjarne habla de RAII ( alrededor de ~ 30min)

Todas las plantillas necesarias (shared_ptr, unique_ptr, weak_ptr) son parte de la biblioteca estándar C ++ 11

Tengo otra situación en la que creo que es perfectamente razonable llamar al destructor.

Al escribir un tipo de método “Restablecer” para restaurar un objeto a su estado inicial, es perfectamente razonable llamar al Destructor para eliminar los datos antiguos que se restablecen.

 class Widget { private: char* pDataText { NULL }; int idNumber { 0 }; public: void Setup() { pDataText = new char[100]; } ~Widget() { delete pDataText; } void Reset() { Widget blankWidget; this->~Widget(); // Manually delete the current object using the dtor *this = blankObject; // Copy a blank object to the this-object. } };