Entiendo la necesidad de un destructor virtual. Pero, ¿por qué necesitamos un destructor virtual puro? En uno de los artículos de C ++, el autor ha mencionado que usamos un destructor virtual puro cuando queremos hacer un resumen de clase.
Pero podemos hacer un resumen de clase haciendo que cualquiera de las funciones miembro sea puramente virtual.
Entonces mis preguntas son
¿Cuándo hacemos realmente que un destructor sea virtual puro? ¿Alguien puede dar un buen ejemplo en tiempo real?
Cuando estamos creando clases abstractas, ¿es una buena práctica hacer que el destructor también sea puramente virtual? Si es así ... entonces ¿por qué?
c++
destructor
pure-virtual
marca
fuente
fuente
Respuestas:
Probablemente, la verdadera razón por la que se permiten destructores virtuales puros es que prohibirlos significaría agregar otra regla al lenguaje y no hay necesidad de esta regla ya que no pueden producirse efectos nocivos al permitir un destructor virtual puro.
No, el viejo virtual es suficiente.
Si crea un objeto con implementaciones predeterminadas para sus métodos virtuales y desea que sea abstracto sin obligar a nadie a anular ningún método específico , puede hacer que el destructor sea virtual puro. No veo mucho sentido, pero es posible.
Tenga en cuenta que dado que el compilador generará un destructor implícito para las clases derivadas, si el autor de la clase no lo hace, las clases derivadas no serán abstractas. Por lo tanto, tener el destructor virtual puro en la clase base no hará ninguna diferencia para las clases derivadas. Solo hará que la clase base sea abstracta (gracias por el comentario de @kappa ).
También se puede suponer que cada clase derivada probablemente necesitaría tener un código de limpieza específico y usar el destructor virtual puro como un recordatorio para escribir uno, pero esto parece artificial (y no forzado).
Nota: El destructor es el único método que, incluso si es virtual puro, tiene que tener una implementación para instanciar clases derivadas (sí, las funciones virtuales puras pueden tener implementaciones).
fuente
foof::bar
si quieres verlo por ti mismo.Todo lo que necesita para una clase abstracta es al menos una función virtual pura. Cualquier función servirá; pero sucede que el destructor es algo que cualquier clase tendrá, por lo que siempre está ahí como candidato. Además, hacer que el destructor sea puramente virtual (en lugar de solo virtual) no tiene efectos secundarios de comportamiento que no sean hacer que la clase sea abstracta. Como tal, muchas guías de estilo recomiendan que el diseño virtual puro se use de manera consistente para indicar que una clase es abstracta, si no por otra razón que proporciona un lugar consistente para que alguien que lee el código pueda ver si la clase es abstracta.
fuente
Si desea crear una clase base abstracta:
... es más fácil hacer que la clase sea abstracta haciendo que el destructor sea puramente virtual y proporcionándole una definición (cuerpo del método).
Para nuestro hipotético ABC:
Usted garantiza que no puede ser instanciado (incluso interno a la clase en sí, es por eso que los constructores privados pueden no ser suficientes), obtiene el comportamiento virtual que desea para el destructor, y no tiene que encontrar y etiquetar otro método que no No necesita envío virtual como "virtual".
fuente
De las respuestas que leí a su pregunta, no pude deducir una buena razón para usar un destructor virtual puro. Por ejemplo, la siguiente razón no me convence en absoluto:
En mi opinión, los destructores virtuales puros pueden ser útiles. Por ejemplo, suponga que tiene dos clases myClassA y myClassB en su código, y que myClassB hereda de myClassA. Por las razones mencionadas por Scott Meyers en su libro "C ++ más eficaz", elemento 33 "Hacer abstractas las clases que no son hojas", es una mejor práctica crear una clase abstracta myAbstractClass de la que heredan myClassA y myClassB. Esto proporciona una mejor abstracción y evita que surjan algunos problemas con, por ejemplo, copias de objetos.
En el proceso de abstracción (de crear la clase myAbstractClass), puede ser que ningún método de myClassA o myClassB sea un buen candidato para ser un método virtual puro (que es un requisito previo para que myAbstractClass sea abstracto). En este caso, usted define el destructor de la clase abstracta virtual puro.
De aquí en adelante, un ejemplo concreto de algún código que yo mismo he escrito. Tengo dos clases, Numerics / PhysicsParams que comparten propiedades comunes. Por lo tanto, les dejo heredar de la clase abstracta IParams. En este caso, no tenía absolutamente ningún método disponible que pudiera ser puramente virtual. El método setParameter, por ejemplo, debe tener el mismo cuerpo para cada subclase. La única opción que tuve fue hacer que el destructor de IParams sea puramente virtual.
fuente
IParam
está protegido, como se señaló en otro comentario.Si desea detener la creación de instancias de la clase base sin realizar ningún cambio en su clase derivada ya implementada y probada, implemente un destructor virtual puro en su clase base.
fuente
Aquí quiero decir cuándo necesitamos un destructor virtual y cuándo necesitamos un destructor virtual puro
Cuando desee que nadie pueda crear el objeto de la clase Base directamente, use un destructor virtual puro
virtual ~Base() = 0
. Por lo general, se requiere al menos una función virtual pura, tomemosvirtual ~Base() = 0
como esta función.Cuando no necesita lo anterior, solo necesita la destrucción segura del objeto de clase Derivado
Base * pBase = new Derived (); eliminar pBase; no se requiere destructor virtual puro, solo el destructor virtual hará el trabajo.
fuente
Te estás metiendo en hipotéticos con estas respuestas, por lo que intentaré hacer una explicación más simple y realista para mayor claridad.
Las relaciones básicas del diseño orientado a objetos son dos: IS-A y HAS-A. No los inventé. Así se llaman.
IS-A indica que un objeto particular se identifica como perteneciente a la clase que está por encima de él en una jerarquía de clases. Un objeto banana es un objeto de fruta si es una subclase de la clase de fruta. Esto significa que en cualquier lugar donde se pueda usar una clase de fruta, se puede usar un plátano. Sin embargo, no es reflexivo. No puede sustituir una clase base por una clase específica si se requiere esa clase específica.
Has-a indicó que un objeto es parte de una clase compuesta y que existe una relación de propiedad. Significa en C ++ que es un objeto miembro y, como tal, corresponde a la clase propietaria deshacerse de él o entregar la propiedad antes de destruirse.
Estos dos conceptos son más fáciles de realizar en lenguajes de herencia única que en un modelo de herencia múltiple como c ++, pero las reglas son esencialmente las mismas. La complicación surge cuando la identidad de la clase es ambigua, como pasar un puntero de clase Banana a una función que toma un puntero de clase Fruit.
Las funciones virtuales son, en primer lugar, una cosa de tiempo de ejecución. Es parte del polimorfismo porque se usa para decidir qué función ejecutar en el momento en que se llama en el programa en ejecución.
La palabra clave virtual es una directiva del compilador para vincular funciones en un cierto orden si existe ambigüedad sobre la identidad de la clase. Las funciones virtuales siempre están en las clases principales (hasta donde yo sé) e indican al compilador que el enlace de las funciones miembro a sus nombres debe tener lugar primero con la función de subclase y después con la función de clase principal.
Una clase Fruit podría tener una función virtual color () que devuelve "NINGUNO" de forma predeterminada. La función Banana class color () devuelve "AMARILLO" o "MARRÓN".
Pero si la función que toma un puntero Fruit llama a color () en la clase Banana que se le envió, ¿qué función color () se invoca? La función normalmente llamaría Fruit :: color () para un objeto Fruit.
Eso sería el 99% de las veces no lo que se pretendía. Pero si Fruit :: color () se declara virtual, entonces Banana: color () se llamaría para el objeto porque la función color () correcta estaría vinculada al puntero de Fruit en el momento de la llamada. El tiempo de ejecución verificará a qué objeto apunta el puntero porque se marcó virtual en la definición de la clase Fruit.
Esto es diferente de anular una función en una subclase. En ese caso, el puntero de Fruit llamará a Fruit :: color () si todo lo que sabe es que es un puntero IS-A a Fruit.
Así que ahora surge la idea de una "función virtual pura". Es una frase bastante desafortunada ya que la pureza no tiene nada que ver con eso. Significa que se pretende que nunca se invoque el método de la clase base. De hecho, no se puede invocar una función virtual pura. Sin embargo, aún debe definirse. Debe existir una firma de función. Muchos codificadores hacen una implementación vacía {} para completar, pero el compilador generará una internamente si no. En ese caso, cuando se llama a la función incluso si el puntero es a Fruit, se llamará a Banana :: color (), ya que es la única implementación de color () que existe.
Ahora la pieza final del rompecabezas: constructores y destructores.
Los constructores virtuales puros son ilegales, completamente. Eso acaba de salir.
Pero los destructores virtuales puros funcionan en el caso de que desee prohibir la creación de una instancia de clase base. Solo se pueden crear instancias de subclases si el destructor de la clase base es puramente virtual. la convención es asignarlo a 0.
Tienes que crear una implementación en este caso. El compilador sabe que esto es lo que está haciendo y se asegura de hacerlo correctamente, o se queja poderosamente de que no puede vincularse a todas las funciones que necesita para compilar. Los errores pueden ser confusos si no está en el camino correcto en cuanto a cómo está modelando su jerarquía de clases.
En este caso, está prohibido crear instancias de Fruit, pero se le permite crear instancias de Banana.
Una llamada para eliminar el puntero de Fruit que apunta a una instancia de Banana llamará primero a Banana :: ~ Banana () y luego a Fuit :: ~ Fruit (), siempre. Porque no importa qué, cuando llama a un destructor de subclase, debe seguir el destructor de la clase base.
¿Es un mal modelo? Es más complicado en la fase de diseño, sí, pero puede garantizar que se realice el enlace correcto en tiempo de ejecución y que se realice una función de subclase donde exista ambigüedad sobre exactamente a qué subclase se está accediendo.
Si escribe C ++ para que solo pase punteros de clase exactos sin punteros genéricos ni ambiguos, entonces las funciones virtuales no son realmente necesarias. Pero si necesita flexibilidad de tipos en tiempo de ejecución (como en Apple Banana Orange ==> Fruit) las funciones se vuelven más fáciles y más versátiles con menos código redundante. Ya no tiene que escribir una función para cada tipo de fruta, y sabe que cada fruta responderá a color () con su propia función correcta.
Espero que esta larga explicación solidifique el concepto en lugar de confundir las cosas. Hay muchos buenos ejemplos por ahí para mirar, mirar lo suficiente y ejecutarlos y meterse con ellos y lo obtendrás.
fuente
Este es un tema de una década de antigüedad :) Lea los últimos 5 párrafos del Artículo # 7 en el libro "Efectivo C ++" para obtener detalles, comienza desde "De vez en cuando puede ser conveniente darle a una clase un destructor virtual puro ..."
fuente
Solicitó un ejemplo, y creo que lo siguiente proporciona una razón para un destructor virtual puro. Espero respuestas sobre si esta es una buena razón ...
No quiero que nadie sea capaz de lanzar el
error_base
tipo, pero los tipos de excepciónerror_oh_shucks
yerror_oh_blast
tener una funcionalidad idéntica y no quiero escribirlo dos veces. La complejidad de pImpl es necesaria para evitar exponermestd::string
a mis clientes, y el uso destd::auto_ptr
necesita el constructor de copias.El encabezado público contiene las especificaciones de excepción que estarán disponibles para el cliente para distinguir los diferentes tipos de excepción lanzados por mi biblioteca:
Y aquí está la implementación compartida:
La clase exception_string, mantenida en privado, oculta std :: string de mi interfaz pública:
Mi código arroja un error como:
El uso de una plantilla para
error
es un poco gratuito. Ahorra un poco de código a expensas de requerir que los clientes detecten errores como:fuente
Tal vez hay otro CASO DE USO REAL de destructor virtual puro que en realidad no puedo ver en otras respuestas :)
Al principio, estoy completamente de acuerdo con la respuesta marcada: es porque prohibir el destructor virtual puro necesitaría una regla adicional en la especificación del lenguaje. Pero todavía no es el caso de uso que Mark está pidiendo :)
Primero imagina esto:
y algo como:
Simplemente, tenemos una interfaz
Printable
y algún "contenedor" que contiene cualquier cosa con esta interfaz. Creo que aquí está bastante claro por qué elprint()
método es puramente virtual. Podría tener algún cuerpo, pero en caso de que no haya una implementación predeterminada, virtual puro es una "implementación" ideal (= "debe ser proporcionada por una clase descendiente").Y ahora imagine exactamente lo mismo, excepto que no es para imprimir sino para destruir:
Y también podría haber un contenedor similar:
Es un caso de uso simplificado de mi aplicación real. La única diferencia aquí es que se utilizó el método "especial" (destructor) en lugar de "normal"
print()
. Pero la razón por la que es puramente virtual sigue siendo la misma: no hay un código predeterminado para el método. Un poco confuso podría ser el hecho de que DEBE haber algún destructor efectivo y el compilador realmente genera un código vacío para él. Pero desde la perspectiva de un programador, la virtualidad pura todavía significa: "No tengo ningún código predeterminado, debe ser proporcionado por clases derivadas".Creo que no es una gran idea aquí, solo una explicación más de que la virtualidad pura funciona de manera realmente uniforme, también para los destructores.
fuente
1) Cuando desee requerir que las clases derivadas hagan la limpieza. Esto es raro.
2) No, pero quieres que sea virtual.
fuente
tenemos que hacer que el destructor sea virtual debido al hecho de que, si no hacemos que el destructor sea virtual, entonces el compilador solo destruirá el contenido de la clase base, n todas las clases derivadas permanecerán sin cambios, el compilador porque no llamará al destructor de ningún otro clase excepto la clase base.
fuente
delete
a un puntero a la clase base cuando de hecho apunta a su derivada.