¿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 Integer
clase 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 vtable
memoria 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 vptr
almacenado 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 vtable
lado 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 object
clase 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 Integer
también tiende a requerir 16 bytes de memoria en plataformas de 64 bits como resultado de este tipo de vptr
metadatos asociados por instancia, y es típicamente imposible en Java envolver algo como un solo int
en 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::vector
evita 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 Standards
capí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).