2009-06-22 21 views
65

Supongamos que tenemos un (juguete) C++ clase tales como los siguientes:¿Un constructor o destructor 'vacío' hará lo mismo que el generado?

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

Desde no se define ningún destructor, un C++ compilador debe 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 compilador nos proporciona), definiremos un destructor vacío, es decir.

Foo::~Foo() { } 

¿hacen lo mismo que el generado por el compilador? ¿Qué tal un constructor vacío, es decir, Foo::Foo() { }?

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

+0

He modificado esta pregunta un poco para convertir la edición posterior a una verdadera parte de la pregunta. Si hay errores de sintaxis en las partes que edité, grítame, no el asker original. @ Andrew, si sientes que he cambiado demasiado tu pregunta, no dudes en revertirla; si le gusta el cambio pero cree que no fue suficiente, obviamente puede editar su propia pregunta. –

Respuesta

112

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 programa está defectuoso. Considere esto

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

Eso está bien, siempre y cuando su no requieren para destruir un objeto de tipo B (y por lo tanto, implícitamente del tipo A) - como si nunca llama a eliminar en un objeto creado de forma dinámica, o nunca creaste un objeto de ella en primer lugar. Si lo hace, entonces el compilador mostrará un diagnóstico apropiado. Ahora bien, si usted proporciona una forma explícita

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

que uno va a tratar de llamar implícitamente el destructor de la clase base, y causará un diagnóstico ya en tiempo 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 a este miembro puntero inteligente

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

Asumamos que el objeto de tipo C se crea en la definición del constructor de la a en el archivo .cpp, que también contiene la definición de la estructura C. Ahora, si usa struct A y requiere la destrucción de un objeto A, el compilador 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.

Esto en realidad 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, conocerá la definición de struct C y puede llamar correctamente a su destructor.

struct C; 
struct A { 
    auto_ptr<C> 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 su constructor se invoca en ciertas maneras.

Otro punto donde hace una diferencia en el C++ actual es cuando desea utilizar memset y amigos 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 ha pedido a los constructores: Bueno, para estas cosas, las mismas cosas son ciertas. 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 compilador debe llamar al destructor. de los miembros. This answer hace un cierto uso de la definición implícita de constructores por defecto.

Además, lo mismo es cierto para la visibilidad y la PODness que he dicho 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, el siguiente es siempre verdad

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

Mientras que el siguiente es un comportamiento indefinido, porque b no se ha inicializado (el constructor que omite). 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 el uso de esta sintaxis en new, como new A() (tenga en cuenta los paréntesis al final - si se omiten valor de inicialización no se hace, y puesto que no hay ningún usuario declaró constructor que podría inicializarlo , a se dejarán sin inicializar).

+0

+1 para la mención de punteros automáticos declarados hacia adelante y el destructor automático. Un problema común cuando comienzas a declarar cosas. –

+1

Su primer ejemplo es un poco extraño. La B que has escrito no se puede usar en absoluto (la nueva sería un error, cualquier conversión a una sería un comportamiento indefinido, ya que no es POD). –

+0

Además, A(). A == 0 solo es válido para estática. Una variable local de tipo A no se inicializará. –

8

Sí, ese destructor vacío es el mismo que el generado automáticamente. Siempre he dejado que el compilador 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.

11

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.

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

+1

Una nota en 3: La especificación de excepción no siempre está vacía en un destructor implícitamente definido, como se indica en [except.spec]. – dalle

+0

@dalle +1 en el comentario - gracias por llamar la atención sobre eso - está en lo cierto, si Foo hubiera derivado de clases base, cada una con destructores no implícitos con especificaciones de excepción - el dtor implícito de Foo hubiera "heredado" la unión de esas excepciones especificaciones - en este caso, dado que no hay herencia, la especificación de excepción del dtor implícito pasa a ser throw(). –

1

Yo diría que es mejor poner la declaración vacía, le dice a los futuros mantenedores que no fue un descuido, y realmente significaba usar el predeterminado.

2

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

virtual ~Foo() { } 

perdiendo destructor virtual puede conducir a la pérdida de memoria debido a las personas que heredan de la clase Foo puede ¡¡¡no me he dado cuenta de que su destructor nunca será llamado !!

0

Una definición vacío está bien ya que la definición se puede hacer referencia

virtual ~GameManager() { };
La declaración vacía es engañosamente similar en apariencia
virtual ~GameManager();
todavía invita a la temida ninguna definición para destructor virtual error
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

16

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

Al observar el ensamblaje generado para algunos códigos que utilizan plantillas de expresión, me di cuenta de que en el modo de publicación, 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é los diversos Compiler Warnings That Are Off by Default. La advertencia C4714 es particularmente interesante. Es emitido por el compilador cuando una función marcada con __forceinlineno se inserta, sin embargo,.

He habilitado la advertencia C4714 y marqué el operador con __forceinline y pude verificar que el compilador informa que no pudo alinear la llamada al operador.

Entre las razones que se describen en la documentación, el compilador falla a inline una función marcada con __forceinline para:

Las funciones que devuelven un objeto desenrollarse por valor cuando -GX/EHS/EHa está en

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 compilador y al I avoid using exception specifications anyway. Al comentar el destructor vacío, el compilador completa el código.

Lo que quiero decir 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 especificación de excepción vacía `/ * throw () */para indicar que el destructor no puede lanzar.

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

Espero que ayude.

Cuestiones relacionadas