¿Alguna vez hay una buena razón para no declarar un destructor virtual para una clase? ¿Cuándo debería evitar específicamente escribir uno?
¿Alguna vez hay una buena razón para no declarar un destructor virtual para una clase? ¿Cuándo debería evitar específicamente escribir uno?
Respuestas:
No es necesario utilizar un destructor virtual cuando se cumple alguna de las siguientes condiciones:
No hay ninguna razón específica para evitarlo a menos que esté realmente tan presionado por la memoria.
Para responder a la pregunta de manera explícita, es decir, cuando en caso de que no se declare un destructor virtual.
C ++ '98 / '03
Agregar un destructor virtual podría cambiar su clase de ser POD (datos antiguos sin formato) * o agregada a no POD. Esto puede evitar que su proyecto se compile 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, tal cambio también puede causar un comportamiento indefinido donde la clase se usa de una manera que requiere un POD, por ejemplo, pasándola a través de un parámetro de puntos suspensivos o usándola con memcpy.
void bar (...);
void foo (A & a) {
bar (a); // Undefined behavior if virtual dtor declared
}
[* Un tipo POD es un tipo que tiene garantías específicas sobre su distribución de memoria. El estándar realmente solo dice que si copiara desde un objeto con tipo POD en una matriz de caracteres (o caracteres sin firmar) y viceversa, el resultado será el mismo que el del 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 se admite condicionalmente con semántica definida por la implementación (N3937 - ~ C ++ '14 - 5.2.2 / 7):
... Pasar un argumento potencialmente evaluado de tipo de clase (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 implementación- semántica definida.
Declarar un destructor que =default
no sea significa que no es trivial (12.4 / 5)
... Un destructor es trivial si no es proporcionado por el usuario ...
Otros cambios en C ++ moderno 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 crear una instancia en el montón o almacenar un puntero a la clase base. Ambas son operaciones extremadamente comunes y a menudo filtrarán recursos silenciosamente si el destructor no se declara virtual.
Se necesita un destructor virtual siempre que haya alguna posibilidad de que se delete
pueda llamar 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, supongamos 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
con muchos objetos en un bucle cerrado, la sobrecarga de rendimiento de llamar a una función virtual (incluso una que esté vacía) puede ser notable. Por lo general, el compilador no puede incluir estas llamadas en línea y el procesador puede 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 función virtual.
Entonces, si su programa implica asignar 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 virtual el dtor.
No todas las clases de C ++ son adecuadas para su uso como 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, cualquier método que una subclase posiblemente quiera anular (lo que podría significar que todos los métodos públicos, más algunos potencialmente protegidos utilizados 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 anima a la gente a utilizar su clase de forma incorrecta.
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, y mucho menos a subclases de ella, entonces lo está haciendo mal.
Una buena razón para no declarar un destructor como virtual es cuando esto evita que su clase tenga que agregar una tabla de funciones virtuales, y debe evitarlo siempre que sea posible.
Sé que mucha gente prefiere declarar siempre a los destructores como virtuales, solo para estar seguros. Pero si su clase no tiene ninguna otra función virtual, entonces realmente no tiene sentido tener un destructor virtual. Incluso si le da su clase a otras personas que luego derivan otras clases de ella, entonces no tendrían ninguna razón para llamar a delete en un puntero que fue ascendente a su clase, y si lo hacen, consideraría esto 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 entonces usted, o los otros chicos, con suerte sepan 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 borrar objetos derivados!"
Si tiene una clase muy pequeña con una gran cantidad de instancias, la sobrecarga de un puntero vtable puede marcar una diferencia en el uso de memoria de su programa. Siempre que su clase no tenga ningún otro método virtual, hacer que el destructor no sea virtual ahorrará esa sobrecarga.
Por lo general, declaro el destructor virtual, pero si tiene un código de rendimiento crítico que se usa 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 la forma en que destruye esos objetos si usa la herencia, o destruirá solo la mitad del objeto.
Tenga en cuenta que la búsqueda de la tabla virtual ocurre para un objeto si algún método en ese objeto es virtual. Entonces, no tiene sentido eliminar la especificación virtual en un destructor si tiene otros métodos virtuales en la clase.
Si absolutamente debe asegurarse de que su clase no tenga 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 vtable para evitar la sobrecarga de funciones porque estas clases se usan específicamente en el ciclo interno de muchas aplicaciones de alto rendimiento.
Una 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 de forma polimórfica a través de la interfaz de clase base, entonces debe comportarse virtualmente y ser virtual.
El destructor no tiene necesidad de 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 es necesario eliminar los punteros de la clase base .
La respuesta de rendimiento es la única que conozco que tiene posibilidades de ser cierta. Si ha medido y ha descubierto que la desvirtualización de sus destructores realmente acelera las cosas, entonces probablemente tenga otras cosas en esa clase que también necesiten acelerar, 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ía 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.