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?
c++
virtual-functions
ggrr
fuente
fuente
Respuestas:
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ónnew
ing 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.
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.
fuente
Por cierto,
Para una clase base con deleción polimórfica.
fuente
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:
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:
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. Elvptr
almacenado por instancia luego apunta a esta clase específicavtable
. 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, estevtable
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 JavaInteger
también tiende a requerir 16 bytes de memoria en plataformas de 64 bits como resultado de este tipo devptr
metadatos asociados por instancia, y es típicamente imposible en Java envolver algo como un soloint
en una clase sin pagar un tiempo de ejecución costo de rendimiento por ello.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.
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í:
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: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).
fuente
¿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.
fuente
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:
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.
fuente
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.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:
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 usandostd::make_shared<Derived>()
. Está bien usarlostd::shared_ptr<Base>
siempre que el puntero inicial sea astd::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
final
en 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.fuente
Declarar un destructor
virtual
solo es necesario cuando planeas hacer tuclass
heredable. Por lo general, las clases de la biblioteca estándar (comostd::string
) no proporcionan un destructor virtual y, por lo tanto, no están destinadas a la subclasificación.fuente
delete
un puntero a una clase base.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
final
en 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.
fuente
You **should** have a virtual destructor if your class is intended as a base-class
es 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