¿Cuándo NO utilizar destructores virtuales?


48

Creí que busqué muchas veces sobre destructores virtuales, la mayoría menciona el propósito de los destructores virtuales y por qué necesita destructores virtuales. También creo que en la mayoría de los casos los destructores deben ser virtuales.

Entonces la pregunta es: ¿por qué c ++ no establece todos los destructores virtuales de forma predeterminada? o en otras preguntas:

¿Cuándo NO necesito usar destructores virtuales?

¿En qué caso NO debería usar destructores virtuales?

¿Cuál es el costo de usar destructores virtuales si lo uso incluso si no es necesario?


66
¿Y qué pasa si no se supone que tu clase sea heredada? Mire muchas de las clases de biblioteca estándar, pocas tienen funciones virtuales porque no están diseñadas para ser heredadas.
Algún tipo programador

44
También creo que en la mayoría de los casos los destructores deben ser virtuales. No. De ningún modo. Solo aquellos que abusan de la herencia (en lugar de favorecer la composición) piensan que sí. He visto aplicaciones completas con solo un puñado de clases base y funciones virtuales.
Matthieu M.

1
@underscore_d Con implementaciones típicas, se generaría código adicional para cualquier clase polimórfica a menos que todas esas cosas implícitas se hayan desvirtualizado y optimizado. Dentro de las ABI comunes, esto implica al menos una vtable para cada clase. El diseño de la clase también tiene que ser cambiado. No puede regresar de manera confiable una vez que haya publicado una clase como parte de alguna interfaz pública, porque cambiarla nuevamente rompería la compatibilidad ABI, ya que obviamente es malo (si es posible) esperar la desvirtualización como contratos de interfaz en general.
FrankHB

1
@underscore_d La frase "en tiempo de compilación" es inexacta, pero creo que esto significa que un destructor virtual no puede ser trivial ni con constexpr especificado, por lo que la generación de código adicional es difícil de evitar (a menos que evite totalmente la destrucción de tales objetos) dañaría más o menos el rendimiento del tiempo de ejecución.
FrankHB

2
@underscore_d El "puntero" parece arenque rojo. Posiblemente debería ser un puntero a miembro (que no es un puntero por definición). Con los ABI habituales, un puntero a miembro a menudo no cabe en una palabra de máquina (como punteros típicos), y cambiar una clase de no polimórfico a polimórfico a menudo cambiaría el tamaño del puntero a miembro de esta clase.
FrankHB

Respuestas:


41

Si agrega un destructor virtual a una clase:

  • en la mayoría (¿todas?) implementaciones actuales de C ++, cada instancia de objeto de esa clase necesita almacenar un puntero a la tabla de despacho virtual para el tipo de tiempo de ejecución, y esa tabla de despacho virtual se agrega a la imagen ejecutable

  • la dirección de la tabla de despacho virtual no es necesariamente válida en todos los procesos, lo que puede evitar compartir dichos objetos de forma segura en la memoria compartida

  • tener un puntero virtual incorporado frustra la creación de una clase con diseño de memoria que coincida con algún formato de entrada o salida conocido (por ejemplo, Price_Tick*podría dirigirse directamente a la memoria adecuadamente alineada en un paquete UDP entrante y usarse para analizar / acceder o alterar los datos, o orientados por ubicación newing clase un ejemplo de datos de escritura en un paquete saliente)

  • el destructor se llama a sí mismo, bajo ciertas condiciones, debe despacharse virtualmente y, por lo tanto, fuera de línea, mientras que los destructores no virtuales pueden estar en línea u optimizados si son triviales o irrelevantes para la persona que llama

El argumento "no diseñado para ser heredado de" no sería una razón práctica para no siempre tener un destructor virtual si no fuera peor de una manera práctica como se explicó anteriormente; pero dado que es peor, ese es un criterio importante para cuándo pagar el costo: por defecto, tener un destructor virtual si su clase está destinada a ser utilizada como una clase base . Eso no siempre es necesario, pero garantiza que las clases en la jerarquía se puedan usar más libremente sin un comportamiento accidental indefinido si se invoca un destructor de clase derivado utilizando un puntero o referencia de clase base.

"En la mayoría de los casos, los destructores deben ser virtuales"

No tanto ... muchas clases no tienen esa necesidad. Hay tantos ejemplos de donde es innecesario que se siente tonto enumerarlos, pero solo mire a través de su Biblioteca estándar o diga impulso y verá que hay una gran mayoría de clases que no tienen destructores virtuales. En el impulso 1.53 cuento 72 destructores virtuales de 494.


23

¿En qué caso NO debería usar destructores virtuales?

  1. Para una clase concreta que no quiere ser heredada.
  2. Para una clase base sin deleción polimórfica. Cualquiera de los clientes no debería poder eliminar polimórficamente utilizando un puntero a Base.

Por cierto,

¿En qué caso se deben usar destructores virtuales?

Para una clase base con deleción polimórfica.


77
+1 para el n. ° 2, específicamente sin deleción polimórfica . Si su destructor nunca puede ser invocado a través de un puntero base, hacerlo virtual es innecesario y redundante, especialmente si su clase no era virtual antes (por lo que se vuelve nuevamente hinchada con RTTI). Para protegerse contra cualquier usuario que viole esto, como lo aconsejó Herb Sutter, haría que el dtor de la clase base esté protegido y no sea virtual, de modo que solo pueda ser invocado por / después de un destructor derivado.
underscore_d

@underscore_d en mi opinión, ese es un punto importante que omití en las respuestas, ya que en presencia de herencia, el único caso en el que no necesito un constructor virtual es cuando puedo asegurarme de que nunca sea necesario
anteriormente conocido como

14

¿Cuál es el costo de usar destructores virtuales si lo uso incluso si no es necesario?

El costo de introducir cualquier función virtual a una clase (heredada o parte de la definición de clase) es posiblemente un costo inicial muy elevado (o no dependiendo del objeto) de un puntero virtual almacenado por objeto, de esta manera:

struct Integer
{
    virtual ~Integer() {}
    int value;
};

En este caso, el costo de la memoria es relativamente enorme. El tamaño de memoria real de una instancia de clase ahora a menudo se verá así en arquitecturas de 64 bits:

struct Integer
{
    // 8 byte vptr overhead
    int value; // 4 bytes
    // typically 4 more bytes of padding for alignment of vptr
};

El total es de 16 bytes para esta Integerclase en lugar de solo 4 bytes. Si almacenamos un millón de estos en una matriz, terminamos con 16 megabytes de uso de memoria: dos veces el tamaño de la típica caché de CPU L3 de 8 MB, e iterar a través de dicha matriz repetidamente puede ser muchas veces más lento que el equivalente de 4 megabytes sin el puntero virtual como resultado de errores de caché adicionales y fallas de página.

Sin embargo, este costo de puntero virtual por objeto no aumenta con más funciones virtuales. Puede tener 100 funciones de miembro virtual en una clase y la sobrecarga por instancia aún sería un puntero virtual único.

El puntero virtual suele ser la preocupación más inmediata desde un punto de vista superior. Sin embargo, además de un puntero virtual por instancia, hay un costo por clase. Cada clase con funciones virtuales genera una vtablememoria en la memoria que almacena las direcciones de las funciones que realmente debería llamar (despacho virtual / dinámico) cuando se realiza una llamada de función virtual. El vptralmacenado por instancia luego apunta a esta clase específica vtable. Esta sobrecarga suele ser una preocupación menor, pero puede inflar su tamaño binario y agregar un poco de costo de tiempo de ejecución si esta sobrecarga se pagó innecesariamente por mil clases en una base de código compleja, por ejemplo, este vtablelado del costo en realidad aumenta proporcionalmente con más y Más funciones virtuales en la mezcla.

Los desarrolladores de Java que trabajan en áreas críticas para el rendimiento comprenden muy bien este tipo de sobrecarga (aunque a menudo se describe en el contexto del boxeo), ya que un tipo definido por el usuario de Java hereda implícitamente de una objectclase base central y todas las funciones en Java son implícitamente virtuales (reemplazables) ) en la naturaleza a menos que se indique lo contrario. Como resultado, un Java Integertambién tiende a requerir 16 bytes de memoria en plataformas de 64 bits como resultado de este tipo de vptrmetadatos asociados por instancia, y es típicamente imposible en Java envolver algo como un solo inten una clase sin pagar un tiempo de ejecución costo de rendimiento por ello.

Entonces la pregunta es: ¿por qué c ++ no establece todos los destructores virtuales de manera predeterminada?

C ++ realmente favorece el rendimiento con una mentalidad de "pago por uso" y también una gran cantidad de diseños basados ​​en hardware heredados de C. No quiere incluir innecesariamente los gastos generales necesarios para la generación de vtable y el despacho dinámico para cada clase / instancia involucrada. Si el rendimiento no es una de las razones clave por las que está utilizando un lenguaje como C ++, es posible que se beneficie más de otros lenguajes de programación, ya que gran parte del lenguaje C ++ es menos seguro y más difícil de lo que idealmente podría ser con el rendimiento a menudo La razón clave para favorecer tal diseño.

¿Cuándo NO necesito usar destructores virtuales?

Muy a menudo. Si una clase no está diseñada para ser heredada, entonces no necesita un destructor virtual y solo terminaría pagando una sobrecarga posiblemente grande por algo que no necesita. Del mismo modo, incluso si una clase está diseñada para ser heredada pero nunca elimina instancias de subtipos a través de un puntero base, tampoco requiere un destructor virtual. En ese caso, una práctica segura es definir un destructor no virtual protegido, así:

class BaseClass
{
protected:
    // Disallow deleting/destroying subclass objects through `BaseClass*`.
    ~BaseClass() {}
};

¿En qué caso NO debería usar destructores virtuales?

En realidad, es más fácil cubrir cuándo debe usar destructores virtuales. Muy a menudo, muchas más clases en su base de código no se diseñarán para la herencia.

std::vector, por ejemplo, no está diseñado para ser heredado y, por lo general, no debe ser heredado (diseño muy inestable), ya que eso será propenso a este problema de eliminación del puntero base ( std::vectorevita deliberadamente un destructor virtual) además de problemas de corte de objetos torpes si su La clase derivada agrega cualquier estado nuevo.

En general, una clase que se hereda debe tener un destructor virtual público o uno no virtual protegido. Del C++ Coding Standardscapítulo 50:

50. Haga que los destructores de clase base sean públicos y virtuales, o protegidos y no virtuales. Eliminar o no eliminar; esa es la pregunta: si se debe eliminar mediante un puntero a una Base base, entonces el destructor de Base debe ser público y virtual. De lo contrario, debe ser protegido y no virtual.

Una de las cosas que C ++ tiende a enfatizar implícitamente (porque los diseños tienden a volverse realmente frágiles e incómodos y posiblemente incluso inseguros) es la idea de que la herencia no es un mecanismo diseñado para ser utilizado en el último momento. Es un mecanismo de extensibilidad con el polimorfismo en mente, pero uno que requiere previsión de dónde se necesita extensibilidad. Como resultado, sus clases base deben diseñarse como raíces de una jerarquía de herencia por adelantado, y no algo que herede más adelante como una ocurrencia tardía sin tal previsión por adelantado.

En aquellos casos en los que simplemente desea heredar para reutilizar el código existente, a menudo se recomienda encarecidamente la composición (Principio de reutilización compuesta).


9

¿Por qué c ++ no establece todos los destructores virtuales por defecto? Costo de almacenamiento adicional y llamada de tabla de método virtual. C ++ se utiliza para el sistema, baja latencia, programación rt donde esto podría ser una carga.


Los destructores no deberían usarse en primer lugar en sistemas de tiempo real difíciles, ya que muchos recursos, como la memoria dinámica, no se pueden usar para proporcionar fuertes garantías de plazo
Marco A.

99
@MarcoA. ¿Desde cuándo los destructores implican asignación dinámica de memoria?
chbaker0

@ chbaker0 Usé un 'me gusta'. Simplemente no se usan en mi experiencia.
Marco A.

66
Tampoco tiene sentido que la memoria dinámica no se pueda utilizar en sistemas de tiempo real duros. Es bastante trivial demostrar que un montón preconfigurado con tamaños de asignación fijos y un mapa de bits de asignación asignará memoria o devolverá una condición de falta de memoria en el tiempo que lleva escanear ese mapa de bits.
MSalters

@msalters que me hace pensar: imagina un programa donde el costo de cada operación se almacenó en el sistema de tipos. Permitir verificaciones en tiempo de compilación de garantías en tiempo real.
Yakk

5

Este es un buen ejemplo de cuándo no usar el destructor virtual: De Scott Meyers:

Si una clase no contiene ninguna función virtual, eso es a menudo una indicación de que no debe usarse como clase base. Cuando una clase no está destinada a ser utilizada como una clase base, hacer que el destructor sea virtual suele ser una mala idea. Considere este ejemplo, basado en una discusión en el ARM:

// class for representing 2D points
class Point {
public:
    Point(short int xCoord, short int yCoord);
    ~Point();
private:
    short int x, y;
};

Si un int corto ocupa 16 bits, un objeto Point puede caber en un registro de 32 bits. Además, un objeto Point se puede pasar como una cantidad de 32 bits a funciones escritas en otros lenguajes como C o FORTRAN. Sin embargo, si el destructor de Point se hace virtual, la situación cambia.

En el momento en que agrega un miembro virtual, se agrega un puntero virtual a su clase que apunta a la tabla virtual para esa clase.


If a class does not contain any virtual functions, that is often an indication that it is not meant to be used as a base class.Wut ¿Alguien más recuerda los buenos viejos tiempos, donde se nos permitió usar clases y herencia para construir capas sucesivas de miembros y comportamientos reutilizables, sin tener que preocuparnos por los métodos virtuales? Vamos Scott. Entiendo el punto central, pero eso "a menudo" realmente está llegando.
underscore_d

3

Un destructor virtual agrega un costo de tiempo de ejecución. El costo es especialmente alto si la clase no tiene ningún otro método virtual. El destructor virtual también solo se necesita en un escenario específico, donde un objeto se elimina o se destruye de otro modo a través de un puntero a una clase base. En este caso, el destructor de la clase base debe ser virtual, y el destructor de cualquier clase derivada será implícitamente virtual. Hay algunos escenarios en los que se usa una clase base polimórfica de tal manera que el destructor no necesita ser virtual:

  • Si las instancias de clases derivadas no se asignan en el montón, por ejemplo, solo directamente en la pila o dentro de otros objetos. (Excepto si usa memoria no inicializada y operador de colocación nuevo).
  • Si las instancias de clases derivadas se asignan en el montón, pero la eliminación ocurre solo a través de punteros a la clase más derivada, por ejemplo, hay una std::unique_ptr<Derived>, y el polimorfismo ocurre solo a través de punteros y referencias no propietarias. Otro ejemplo es cuando los objetos se asignan usando std::make_shared<Derived>(). Está bien usarlo std::shared_ptr<Base>siempre que el puntero inicial sea a std::shared_ptr<Derived>. Esto se debe a que los punteros compartidos tienen su propio despacho dinámico para destructores (el eliminador) que no necesariamente se basa en un destructor de clase base virtual.

Por supuesto, cualquier convención para usar objetos solo de las formas antes mencionadas se puede romper fácilmente. Por lo tanto, el consejo de Herb Sutter sigue siendo tan válido como siempre: "Los destructores de clase base deben ser públicos y virtuales, o protegidos y no virtuales". De esa manera, si alguien intenta eliminar un puntero a una clase base con destructor no virtual, lo más probable es que reciba un error de infracción de acceso en el momento de la compilación.

Por otra parte, hay clases que no están diseñadas para ser clases base (públicas). Mi recomendación personal es hacerlos finalen C ++ 11 o superior. Si está diseñado para ser una clavija cuadrada, entonces es probable que no funcione muy bien como una clavija redonda. Esto está relacionado con mi preferencia por tener un contrato de herencia explícito entre la clase base y la clase derivada, para el patrón de diseño NVI (interfaz no virtual), para clases base abstractas en lugar de concretas, y mi aborrecimiento de las variables miembro protegidas, entre otras cosas. , pero sé que todas estas opiniones son controvertidas hasta cierto punto.


1

Declarar un destructor virtualsolo es necesario cuando planeas hacer tu classheredable. Por lo general, las clases de la biblioteca estándar (como std::string) no proporcionan un destructor virtual y, por lo tanto, no están destinadas a la subclasificación.


3
La razón es la subclasificación + uso de polimorfismo. Se requiere un destructor virtual solo si se necesita una resolución dinámica, es decir, una referencia / puntero / lo que sea que la clase maestra en realidad pueda referirse a una instancia de una subclase.
Michel Billaud

2
@MichelBillaud en realidad todavía puedes tener polimorfismo sin dtors virtuales. SOLO se requiere un dtor virtual para la eliminación polimórfica, es decir, invocar deleteun puntero a una clase base.
chbaker0

1

Habrá una sobrecarga en el constructor para crear la vtable (si no tiene otras funciones virtuales, en cuyo caso PROBABLEMENTE, pero no siempre, también debería tener un destructor virtual). Y si no tiene otras funciones virtuales, hace que su objeto sea un puntero más grande de lo que es necesario. Obviamente, el aumento de tamaño puede tener un gran impacto en objetos pequeños.

Hay una lectura de memoria adicional para obtener la vtable y luego llamar a la función indirectamente a través de eso, que es una sobrecarga sobre el destructor no virtual cuando se llama al destructor. Y, por supuesto, como consecuencia, se genera un pequeño código adicional para cada llamada al destructor. Esto es para los casos en que el compilador no puede deducir el tipo real; en aquellos casos en los que puede deducir el tipo real, el compilador no usará la tabla vtable, sino que llamará directamente al destructor.

Usted debe tener un destructor virtual si su clase está pensado como una clase base, en particular, si se puede crear / destruida por alguna otra entidad que el código que se sabe de qué tipo es en la creación, entonces necesita un destructor virtual.

Si no está seguro, use el destructor virtual. Es más fácil eliminar virtual si aparece como un problema que tratar de encontrar el error causado por "no se llama al destructor correcto".

En resumen, no debería tener un destructor virtual si: 1. No tiene ninguna función virtual. 2. No derive de la clase (márquela finalen C ++ 11, de esa manera el compilador le dirá si intenta derivar de ella).

En la mayoría de los casos, la creación y destrucción no es una parte importante del tiempo empleado en un objeto en particular a menos que haya "mucho contenido" (crear una cadena de 1 MB obviamente llevará algún tiempo, porque al menos 1 MB de datos necesita copiarse desde donde se encuentre actualmente). Destruir una cadena de 1 MB no es peor que la destrucción de una cadena de 150B, ambas requerirán desasignar el almacenamiento de la cadena, y no mucho más, por lo que el tiempo que pasa allí suele ser el mismo [a menos que sea una construcción de depuración, donde la desasignación a menudo llena la memoria con un "patrón de envenenamiento", pero no es así como va a ejecutar su aplicación real en producción].

En resumen, hay una pequeña sobrecarga, pero para objetos pequeños, puede hacer la diferencia.

Tenga en cuenta también que los compiladores pueden optimizar la búsqueda virtual en algunos casos, por lo que es solo una penalización

Como siempre en lo que respecta al rendimiento, la huella de la memoria, etc.: comparar y perfilar y medir, comparar los resultados con alternativas y observar dónde se gasta la mayor parte del tiempo / memoria, y no intente optimizar el 90% de código que no se ejecuta mucho [la mayoría de las aplicaciones tienen aproximadamente el 10% del código que tiene una gran influencia en el tiempo de ejecución, y el 90% del código que no tiene mucha influencia en absoluto]. ¡Haga esto en un alto nivel de optimización, de modo que ya tenga la ventaja de que el compilador hace un buen trabajo! Y repita, verifique nuevamente y mejore paso a paso. No intente ser inteligente e intente descubrir qué es importante y qué no lo es, a menos que tenga mucha experiencia con ese tipo particular de aplicación.


1
"será una sobrecarga en el constructor para crear la vtable" : las compilaciones de la vtable generalmente son "creadas" por clase por el compilador, y el constructor solo tiene la sobrecarga de almacenar un puntero en la instancia del objeto en construcción.
Tony

Además ... me refiero a evitar la optimización prematura, pero a la inversa, You **should** have a virtual destructor if your class is intended as a base-classes una simplificación excesiva y pesimización prematura . Esto solo es necesario si a alguien se le permite eliminar una clase derivada a través del puntero a la base. En muchas situaciones, eso no es así. Si sabes que es así, entonces seguro, incurrir en los gastos generales. Lo cual, por cierto, siempre se agrega, incluso si el compilador resuelve estáticamente las llamadas reales. De lo contrario, cuando controlas adecuadamente lo que la gente puede hacer con tus objetos, no vale la pena
underscore_d
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.