¿Cuándo no deberías usar destructores virtuales?

Respuestas:

72

No es necesario utilizar un destructor virtual cuando se cumple alguna de las siguientes condiciones:

  • Sin intención de derivar clases de él.
  • Sin instanciación en el montón
  • Sin intención de almacenar en un puntero de una superclase

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

sep
fuente
25
Ésta no es una buena respuesta. "No hay necesidad" es diferente de "no debería", y "sin intención" es diferente de "hecho imposible".
Programador de Windows
5
También agregue: no hay intención de eliminar una instancia a través de un puntero de clase base.
Adam Rosenfield
9
Esto realmente no responde a la pregunta. ¿Dónde está tu buena razón para no usar un dtor virtual?
mxcl
9
Creo que cuando no hay necesidad de hacer algo, es una buena razón para no hacerlo. Está siguiendo el principio de diseño simple de XP.
Sep
12
Al decir que "no tiene ninguna intención", está haciendo una gran suposición sobre cómo se utilizará su clase. Me parece que la solución más simple en la mayoría de los casos (que, por lo tanto, debería ser la predeterminada) debería ser tener destructores virtuales y solo evitarlos si tiene una razón específica para no hacerlo. Así que todavía tengo curiosidad sobre cuál sería una buena razón.
ckarras
68

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 =defaultno 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
}
Richard Corden
fuente
1
Tienes razón y yo estaba equivocado, el rendimiento no es la única razón. Pero esto demuestra que tenía razón sobre el resto: es mejor que el programador de la clase incluya código para evitar que alguien más herede la clase.
Programador de Windows
querido Richard, ¿podrías comentar un poco más sobre lo que has escrito? No entiendo su punto, pero parece el único punto valioso que he encontrado al buscar en Google) ¿O puede ser que pueda dar un enlace a una explicación más detallada?
John Smith
1
@JohnSmith He actualizado la respuesta. Ojalá esto ayude.
Richard Corden
28

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.

Andy
fuente
3
Y, de hecho, hay una opción de advertencia en gcc que advierte precisamente en ese caso (métodos virtuales pero no dtor virtual).
CesarB
6
¿No corre el riesgo de perder memoria si se deriva de la clase, independientemente de si tiene otras funciones virtuales?
Mag Roader
1
Estoy de acuerdo con mag. Este uso de un destructor virtual o un método virtual son requisitos separados. El destructor virtual proporciona la capacidad de una clase para realizar una limpieza (por ejemplo, eliminar memoria, cerrar archivos, etc.) Y también garantiza que se llame a los constructores de todos sus miembros.
user48956
7

Se necesita un destructor virtual siempre que haya alguna posibilidad de que se deletepueda 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 Bes 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 deletecon 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.

Jay Conrod
fuente
"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". debería enfatizarse más en cada respuesta que veo
csguy
5

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.

mxcl
fuente
1
Es solo una puntilla, pero en estos días un puntero suele ser de 64 bits en lugar de 32.
Head Geek
5

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.

Steve Jessop
fuente
2
El uso polimórfico no implica la eliminación polimórfica. Hay muchos casos de uso para que una clase tenga métodos virtuales pero no un destructor virtual. Considere un cuadro de diálogo típico definido estáticamente, en casi cualquier kit de herramientas de GUI. La ventana principal destruirá los objetos secundarios y conoce el tipo exacto de cada uno; sin embargo, todas las ventanas secundarias también se usarán polimórficamente en cualquier número de lugares, como pruebas de acceso, dibujo, API de accesibilidad que obtienen el texto para texto. motores de voz, etc.
Ben Voigt
4
Es cierto, pero el interrogador pregunta cuándo debe evitar específicamente un destructor virtual. Para el cuadro de diálogo que describe, un destructor virtual no tiene sentido, pero en mi opinión no es dañino. No estoy seguro de estar seguro de que nunca necesitaré eliminar un cuadro de diálogo usando un puntero de clase base; por ejemplo, en el futuro, puede que quiera que mi ventana principal cree sus objetos secundarios usando fábricas. Así que no se trata de evitar el destructor virtual, solo que quizás no te molestes en tener uno. Sin embargo, un destructor virtual en una clase no adecuada para la derivación es dañino porque es engañoso.
Steve Jessop
4

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!"

kidfisto
fuente
3

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.

Mark Ransom
fuente
1

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.

Jørn Jensen
fuente
1

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.

Lisa
fuente
0

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 .

hielo
fuente
-7

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.

Programador de Windows
fuente
El polimorfismo ciertamente ralentizará las cosas. Compárelo con una situación en la que necesitamos polimorfismo y optamos por no hacerlo, será aún más lento. Ejemplo: implementamos toda la lógica en el destructor de la clase base, usando RTTI y una declaración de cambio para limpiar los recursos.
Sep
1
En C ++, no es su responsabilidad evitar que herede de sus clases que ha documentado que no son adecuadas para su uso como clases base. Es mi responsabilidad utilizar la herencia con precaución. A menos que la guía de estilo de la casa diga lo contrario, por supuesto.
Steve Jessop
1
... hacer que el destructor sea virtual no significa que la clase necesariamente funcione correctamente como clase base. Así que marcarlo como virtual "solo porque", en lugar de hacer esa evaluación, es escribir un cheque que mi código no puede cobrar.
Steve Jessop