Pregunta

¿Hay alguna buena razón para no declarar un destructor virtual para una clase? ¿Cuándo debería evitar específicamente escribir uno?

¿Fue útil?

Solución

No hay necesidad de usar un destructor virtual cuando cualquiera de las siguientes afirmaciones es verdadera:

  • Ninguna intención de derivar clases de ella
  • Sin instanciación en el montón
  • Sin intención de almacenar en un puntero de una superclase

No hay una razón específica para evitarlo a menos que esté realmente tan presionado por la memoria.

Otros consejos

Para responder la pregunta explícitamente, es decir, cuándo debería no declarar un destructor virtual.

C ++ '98 / '03

Agregar un destructor virtual podría cambiar su clase de ser POD (datos antiguos simples) * o agregado a no POD. Esto puede detener la compilación de su proyecto si su tipo de clase es agregado inicializado en alguna parte.

struct A {
  // virtual ~A ();
  int i;
  int j;
};
void foo () { 
  A a = { 0, 1 };  // Will fail if virtual dtor declared
}

En un caso extremo, dicho cambio también puede causar un comportamiento indefinido en el que la clase se utiliza de una manera que requiere un POD, p. pasarlo a través de un parámetro de puntos suspensivos o usarlo con memcpy.

void bar (...);
void foo (A & a) { 
  bar (a);  // Undefined behavior if virtual dtor declared
}

[* Un tipo de POD es un tipo que tiene garantías específicas sobre su diseño de memoria. El estándar realmente solo dice que si copiara de un objeto con tipo POD en una matriz de caracteres (o caracteres sin signo) y viceversa, entonces el resultado será el mismo que el objeto original.]

C ++ moderno

En versiones recientes de C ++, el concepto de POD se dividió entre el diseño de la clase y su construcción, copia y destrucción.

Para el caso de puntos suspensivos, ya no es un comportamiento indefinido, ahora es compatible con la semántica definida por la implementación (N3937 - ~ C ++ '14 - 5.2.2 / 7):

  

... Pasar un argumento de tipo de clase potencialmente evaluado (Cláusula 9) que tiene un constructor de copia no trivial, un constructor de movimiento no trivial o un destructor on trivial, sin parámetro correspondiente, es condicionalmente compatible con semántica definida por la implementación.

Declarar un destructor que no sea = default significa que no es trivial (12.4 / 5)

  

... Un destructor es trivial si no es proporcionado por el usuario ...

Otros cambios en Modern C ++ reducen el impacto del problema de inicialización agregada ya que se puede agregar un constructor:

struct A {
  A(int i, int j);
  virtual ~A ();
  int i;

  int j;
};
void foo () { 
  A a = { 0, 1 };  // OK
}

Declaro un destructor virtual si y solo si tengo métodos virtuales. Una vez que tengo métodos virtuales, no confío en mí mismo para evitar instanciarlo en el montón o almacenar un puntero a la clase base. Ambas son operaciones extremadamente comunes y a menudo perderán recursos silenciosamente si el destructor no se declara virtual.

Se necesita un destructor virtual siempre que exista la posibilidad de que se invoque delete en un puntero a un objeto de una subclase con el tipo de su clase. Esto asegura que se llame al destructor correcto en tiempo de ejecución sin que el compilador tenga que conocer la clase de un objeto en el montón en tiempo de compilación. Por ejemplo, suponga que B es una subclase de A :

A *x = new B;
delete x;     // ~B() called, even though x has type A*

Si su código no es crítico para el rendimiento, sería razonable agregar un destructor virtual a cada clase base que escriba, solo por seguridad.

Sin embargo, si se encuentra delete ing de muchos objetos en un ciclo cerrado, la sobrecarga de rendimiento de llamar a una función virtual (incluso una que esté vacía) podría ser notable. El compilador generalmente no puede alinear estas llamadas, y el procesador podría tener dificultades para predecir a dónde ir. Es poco probable que esto tenga un impacto significativo en el rendimiento, pero vale la pena mencionarlo.

Las funciones virtuales significan que cada objeto asignado aumenta el costo de la memoria mediante un puntero de tabla de funciones virtuales.

Entonces, si su programa implica la asignación de una gran cantidad de algún objeto, valdría la pena evitar todas las funciones virtuales para guardar los 32 bits adicionales por objeto.

En todos los demás casos, se ahorrará la miseria de depuración para hacer que el dtor sea virtual.

No todas las clases de C ++ son adecuadas para su uso como una clase base con polimorfismo dinámico.

Si desea que su clase sea adecuada para el polimorfismo dinámico, entonces su destructor debe ser virtual. Además, todos los métodos que una subclase podría querer anular (lo que podría significar que todos los métodos públicos, más potencialmente algunos protegidos internamente) deben ser virtuales.

Si su clase no es adecuada para el polimorfismo dinámico, entonces el destructor no debe marcarse como virtual, porque hacerlo es engañoso. Simplemente alienta a las personas a usar tu clase incorrectamente.

Aquí hay un ejemplo de una clase que no sería adecuada para el polimorfismo dinámico, incluso si su destructor fuera virtual:

class MutexLock {
    mutex *mtx_;
public:
    explicit MutexLock(mutex *mtx) : mtx_(mtx) { mtx_->lock(); }
    ~MutexLock() { mtx_->unlock(); }
private:
    MutexLock(const MutexLock &rhs);
    MutexLock &operator=(const MutexLock &rhs);
};

El objetivo de esta clase es sentarse en la pila para RAII. Si está pasando punteros a objetos de esta clase, por no hablar de las subclases, entonces lo está haciendo mal.

Una buena razón para no declarar un destructor como virtual es cuando esto evita que su clase tenga una tabla de funciones virtuales agregada, y debe evitar eso siempre que sea posible.

Sé que muchas personas prefieren declarar siempre los destructores como virtuales, solo para estar seguros. Pero si su clase no tiene otras funciones virtuales, entonces realmente no tiene sentido tener un destructor virtual. Incluso si le das tu clase a otras personas que luego derivan otras clases de ella, entonces no tendrían ninguna razón para llamar a eliminar en un puntero que se envió a tu clase, y si lo hacen, lo consideraría un error.

De acuerdo, hay una única excepción, es decir, si su clase se usa (mal) para realizar la eliminación polimórfica de objetos derivados, pero luego usted, o los demás, saben que esto requiere un destructor virtual.

Dicho de otra manera, si su clase tiene un destructor no virtual, entonces esta es una declaración muy clara: "¡No me use para eliminar objetos derivados!"

Si tiene una clase muy pequeña con una gran cantidad de instancias, la sobrecarga de un puntero vtable puede marcar la diferencia en el uso de memoria de su programa. Mientras su clase no tenga otros métodos virtuales, hacer que el destructor no sea virtual ahorrará esa sobrecarga.

Por lo general, declaro el destructor virtual, pero si tiene un código crítico de rendimiento que se utiliza en un bucle interno, es posible que desee evitar la búsqueda de la tabla virtual. Eso puede ser importante en algunos casos, como la verificación de colisiones. Pero tenga cuidado con cómo destruye esos objetos si usa la herencia, o destruirá solo la mitad del objeto.

Tenga en cuenta que la búsqueda de tabla virtual ocurre para un objeto si el método any en ese objeto es virtual. Por lo tanto, no tiene sentido eliminar la especificación virtual en un destructor si tiene otros métodos virtuales en la clase.

Si debe asegurarse absolutamente de que su clase no tiene una vtable, entonces tampoco debe tener un destructor virtual.

Este es un caso raro, pero sucede.

El ejemplo más familiar de un patrón que hace esto son las clases DirectX D3DVECTOR y D3DMATRIX. Estos son métodos de clase en lugar de funciones para el azúcar sintáctico, pero las clases intencionalmente no tienen una tabla virtual para evitar la sobrecarga de la función porque estas clases se usan específicamente en el bucle interno de muchas aplicaciones de alto rendimiento.

En la operación que se realizará en la clase base, y que debería comportarse virtualmente, debería ser virtual. Si la eliminación se puede realizar polimórficamente a través de la interfaz de clase base, entonces debe comportarse virtualmente y ser virtual.

El destructor no necesita ser virtual si no tiene la intención de derivar de la clase. E incluso si lo hace, un destructor no virtual protegido es igual de bueno si no se requiere la eliminación de punteros de clase base .

La respuesta de rendimiento es la única que conozco que tiene posibilidades de ser cierta. Si midió y descubrió que la des-virtualización de sus destructores realmente acelera las cosas, entonces probablemente tenga otras cosas en esa clase que también deben acelerarse, pero en este punto hay consideraciones más importantes. Algún día alguien descubrirá que su código les proporcionaría una buena clase base y les ahorrará el trabajo de una semana. Será mejor que se asegure de que hagan el trabajo de esa semana, copiando y pegando su código, en lugar de usar su código como base. Será mejor que se asegure de que algunos de sus métodos importantes sean privados para que nadie pueda heredar de usted.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top