¿Un constructor o destructor “vacío” hará lo mismo que el generado?

Supongamos que tenemos una clase C ++ (de juguete) como la siguiente:

class Foo { public: Foo(); private: int t; }; 

Como no se define ningún destructor, un comstackdor de C ++ debería crear uno automáticamente para la clase Foo. Si el destructor no necesita limpiar ninguna memoria asignada dinámicamente (es decir, podríamos confiar razonablemente en el destructor que el comstackdor nos proporciona), definiremos un destructor vacío, es decir.

 Foo::~Foo() { } 

hacer lo mismo que el generado por el comstackdor? ¿Qué pasa con un constructor vacío, es decir, Foo::Foo() { } ?

Si hay diferencias, ¿dónde existen? Si no, ¿se prefiere un método sobre el otro?

Hará lo mismo (nada, en esencia). Pero no es lo mismo que si no lo escribieras. Porque escribir el destructor requerirá un destructor de clase base en funcionamiento. Si el destructor de la clase base es privado o si por algún otro motivo no puede invocarse, entonces su progtwig está defectuoso. Considera esto

 struct A { private: ~A(); }; struct B : A { }; 

Eso está bien, siempre y cuando no necesites destruir un objeto de tipo B (y, por lo tanto, implícitamente de tipo A), como si nunca llamaras a eliminar en un objeto creado dinámicamente, o nunca creas un objeto de él en El primer lugar. Si lo hace, entonces el comstackdor mostrará un diagnóstico apropiado. Ahora si proporciona uno explícitamente

 struct A { private: ~A(); }; struct B : A { ~B() { /* ... */ } }; 

Ese tratará de llamar implícitamente al destructor de la clase base, y causará un diagnóstico ya en el momento de definición de ~B

Hay otra diferencia que se centra en la definición del destructor y las llamadas implícitas a los miembros destructores. Considere este miembro inteligente del puntero

 struct C; struct A { auto_ptr a; A(); }; 

Supongamos que el objeto de tipo C se crea en la definición del constructor de A en el archivo .cpp , que también contiene la definición de estructura C Ahora, si usa struct A y requiere la destrucción de un objeto A , el comstackdor proporcionará una definición implícita del destructor, como en el caso anterior. Ese destructor también llamará implícitamente al destructor del objeto auto_ptr. Y eso eliminará el puntero que contiene, que apunta al objeto C , ¡sin conocer la definición de C ! Eso apareció en el archivo .cpp donde se define el constructor de struct A.

En realidad, este es un problema común en la implementación del idioma pimpl. La solución aquí es agregar un destructor y proporcionar una definición vacía de él en el archivo .cpp , donde se define la estructura C En el momento en que invoca el destructor de su miembro, sabrá la definición de struct C y puede llamar correctamente a su destructor.

 struct C; struct A { auto_ptr a; A(); ~A(); // defined as ~A() { } in .cpp file, too }; 

Tenga en cuenta que boost::shared_ptr no tiene ese problema: en su lugar, requiere un tipo completo cuando se invoca su constructor de ciertas maneras.

Otro punto donde hace una diferencia en el C ++ actual es cuando desea usar memset y friends en un objeto que tiene un usuario declarado destructor. Dichos tipos ya no son POD (datos antiguos simples), y no se permite copiarlos en bits. Tenga en cuenta que esta restricción no es realmente necesaria, y la próxima versión de C ++ ha mejorado la situación en este sentido, de modo que le permite copiar aún dichos tipos, siempre que no se realicen otros cambios más importantes.


Como usted pidió constructores: Bueno, para estas cosas, lo mismo es cierto. Tenga en cuenta que los constructores también contienen llamadas implícitas a los destructores. En cosas como auto_ptr, estas llamadas (incluso si no se realizan en tiempo de ejecución, la posibilidad pura ya importa aquí) hará el mismo daño que para los destructores, y ocurrirá cuando algo en el constructor arroje, el comstackdor debe llamar al destructor. de los miembros. Esta respuesta hace un uso de la definición implícita de constructores por defecto.

Además, lo mismo es cierto para la visibilidad y PODness que dije sobre el destructor anterior.

Hay una diferencia importante con respecto a la inicialización. Si pone un constructor declarado por el usuario, su tipo ya no recibe la inicialización de valor de los miembros, y le corresponde a su constructor hacer cualquier inicialización que sea necesaria. Ejemplo:

 struct A { int a; }; struct B { int b; B() { } }; 

En este caso, lo siguiente siempre es cierto

 assert(A().a == 0); 

Si bien el siguiente es un comportamiento indefinido, porque b nunca se inicializó (su constructor omitió eso). El valor puede ser cero, pero también puede ser cualquier otro valor extraño. Intentar leer de un objeto no inicializado provoca un comportamiento indefinido.

 assert(B().b == 0); 

Esto también es cierto para usar esta syntax en new , como new A() (observe los paréntesis al final – si se omite la inicialización del valor no se hace, y dado que no hay ningún constructor declarado por el usuario que pueda inicializarlo, a será dejado sin inicializar).

Sé que llego tarde a la discusión, sin embargo, mi experiencia dice que el comstackdor se comporta de manera diferente cuando se enfrenta a un destructor vacío en comparación con un comstackdor generado. Al menos este es el caso con MSVC ++ 8.0 (2005) y MSVC ++ 9.0 (2008).

Cuando miro el ensamblaje generado para algún código haciendo uso de plantillas de expresión, me di cuenta de que en el modo de lanzamiento, la llamada a mi BinaryVectorExpression operator + (const Vector& lhs, const Vector& rhs) nunca se introdujo. (por favor, no preste atención a los tipos exactos y la firma del operador).

Para diagnosticar aún más el problema, habilité las diversas advertencias del comstackdor que están desactivadas de manera predeterminada . La advertencia C4714 es particularmente interesante. Es emitido por el comstackdor cuando una función marcada con __forceinline no se inline de todos modos .

Active la advertencia __forceinline y __forceinline el operador con __forceinline y pude verificar que el comstackdor informa que no pudo alinear la llamada al operador.

Entre los motivos descritos en la documentación, el comstackdor no puede __forceinline una función marcada con __forceinline para:

Funciones que devuelven un objeto no enrollable por valor cuando -GX / EHs / EHa está activado

Este es el caso de mi BinaryVectorExpression operator + (const Vector& lhs, const Vector& rhs) . BinaryVectorExpression se devuelve por valor y, aunque su destructor está vacío, hace que este valor devuelto se considere como un objeto no enrutable. Agregar throw () al destructor no ayudó al comstackdor y , de todos modos, evité utilizar las especificaciones de excepción . Al comentar el destructor vacío, el comstackdor completa el código.

Lo que se lleva es que a partir de ahora, en cada clase, escribo destructores vacíos comentados para que los humanos sepan que el destructor no hace nada a propósito, del mismo modo que la gente comenta la especificación de excepción vacía `/ * throw () * / to indica que el destructor no puede tirar.

 //~Foo() /* throw() */ {} 

Espero que ayude.

El destructor vacío que definió fuera de clase tiene una semántica similar en la mayoría de los aspectos, pero no en todos.

Específicamente, el destructor implícitamente definido
1) es un miembro público en línea (el suyo no está en línea)
2) se denota como un destructor trivial (necesario para hacer tipos triviales que pueden estar en uniones, los tuyos no)
3) tiene una especificación de excepción (throw (), el tuyo no)

Sí, ese destructor vacío es el mismo que el generado automáticamente. Siempre he dejado que el comstackdor los genere automáticamente; No creo que sea necesario especificar el destructor explícitamente a menos que necesite hacer algo inusual: hacerlo virtual o privado, por ejemplo.

Estoy de acuerdo con David, excepto que diría que generalmente es una buena práctica definir un destructor virtual, es decir,

 virtual ~Foo() { } 

Perder el destructor virtual puede conducir a la pérdida de memoria porque las personas que heredan de su clase Foo pueden no haber notado que su destructor nunca será llamado.

Yo diría que es mejor poner la statement vacía, le dice a los futuros mantenedores que no fue un descuido, y realmente quiso utilizar la statement predeterminada.

Una definición vacía está bien ya que la definición puede ser referenciada

 virtual ~GameManager() { }; 

La statement vacía es engañosamente similar en apariencia

  virtual ~ GameManager (); 

sin embargo, invita a la temida definición sin error virtual destructor

 Undefined symbols: "vtable for GameManager", referenced from: __ZTV11GameManager$non_lazy_ptr in GameManager.o __ZTV11GameManager$non_lazy_ptr in Main.o ld: symbol(s) not found