¿Por qué necesitamos un destructor virtual puro en C ++?

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 mencionó que usamos el 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

  1. ¿Cuándo realmente hacemos un destructor puro virtual? ¿Alguien puede dar un buen ejemplo en tiempo real?

  2. Cuando estamos creando clases abstractas, ¿es una buena práctica hacer que el destructor también sea puramente virtual? Si es así … ¿por qué?

  1. Probablemente la verdadera razón por la que se permiten los 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 perjudiciales al permitir un destructor virtual puro.

  2. No, simple viejo virtual es suficiente.

Si crea un objeto con implementaciones predeterminadas para sus métodos virtuales y desea hacerlo abstracto sin forzar a nadie a anular ningún método específico , puede hacer que el destructor sea puro virtual. No veo mucho sentido en eso, pero es posible.

Tenga en cuenta que dado que el comstackdor 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 necesitaría tener un código de limpieza específico y usar el destructor virtual puro como recordatorio para escribir uno, pero esto parece artificial (y no se aplica).

Nota: El destructor es el único método que, incluso si es puramente virtual, tiene que tener una implementación para instanciar las clases derivadas (sí, las funciones virtuales puras pueden tener implementaciones).

struct foo { virtual void bar() = 0; }; void foo::bar() { /* default implementation */ } class foof : public foo { void bar() { foo::bar(); } // have to explicitly call default implementation. }; 

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 puro virtual (en lugar de simplemente virtual) no tiene efectos secundarios de comportamiento más que hacer que la clase sea abstracta. Como tal, muchas guías de estilo recomiendan que el destilador virtual puro se use de manera consistente para indicar que una clase es abstracta, si no es por otra razón que proporciona un lugar consistente en que alguien que lea el código pueda ver si la clase es abstracta.

Si quieres crear una clase base abstracta:

  • eso no se puede instanciar (¡sí, esto es redundante con el término “abstracto”!)
  • pero necesita un comportamiento de destructor virtual (tiene la intención de llevar punteros al ABC en lugar de punteros a los tipos derivados, y eliminar a través de ellos)
  • pero no necesita ningún otro comportamiento de despacho virtual para otros métodos (tal vez no haya otros métodos) considere un contenedor de “recursos” simple y protegido que necesite un constructor / destructor / asignación pero no mucho más)

… es más fácil hacer que la clase sea abstracta haciendo que el destructor sea virtualmente puro y proporcione una definición (cuerpo del método) para él.

Para nuestro hipotético ABC:

Usted garantiza que no se puede crear una instancia (incluso interna para la clase en sí, esta es la razón por la cual los constructores privados pueden no ser suficientes), usted obtiene el comportamiento virtual que desea para el destructor, y no tiene que encontrar y etiquetar otro método que no lo haga No es necesario el despacho virtual como “virtual”.

De las respuestas que leí a su pregunta, no pude deducir una buena razón para usar realmente un destructor virtual puro. Por ejemplo, la siguiente razón no me convence en absoluto:

Probablemente la verdadera razón por la que se permiten los 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 perjudiciales al permitir un destructor virtual puro.

En mi opinión, los destructores virtuales puros pueden ser útiles. Por ejemplo, supongamos 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 efectivo”, el ítem 33 “Hacer abstractas las clases no hojas”, es una mejor práctica crear realmente una clase abstracta myAbstractClass de la cual myClassA y myClassB heredan. Esto proporciona una mejor abstracción y evita algunos problemas que surgen 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 puro de la clase abstracta.

A partir de ahora, 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 a mano 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 convertir el destructor de IParams en virtual puro.

 struct IParams { IParams(const ModelConfiguration& aModelConf); virtual ~IParams() = 0; void setParameter(const N_Configuration::Parameter& aParam); std::map m_Parameters; }; struct NumericsParams : IParams { NumericsParams(const ModelConfiguration& aNumericsConf); virtual ~NumericsParams(); double dt() const; double ti() const; double tf() const; }; struct PhysicsParams : IParams { PhysicsParams(const N_Configuration::ModelConfiguration& aPhysicsConf); virtual ~PhysicsParams(); double g() const; double rho_i() const; double rho_w() const; }; 

Si desea detener la creación de instancias de la clase base sin realizar ningún cambio en la clase derivada ya implementada y probada, implementará un destructor virtual puro en su clase base.

Aquí quiero decir cuándo necesitamos un destructor virtual y cuándo necesitamos un destructor virtual puro

 class Base { public: Base(); virtual ~Base() = 0; // Pure virtual, now no one can create the Base Object directly }; Base::Base() { cout << "Base Constructor" << endl; } Base::~Base() { cout << "Base Destructor" << endl; } class Derived : public Base { public: Derived(); ~Derived(); }; Derived::Derived() { cout << "Derived Constructor" << endl; } Derived::~Derived() { cout << "Derived Destructor" << endl; } int _tmain(int argc, _TCHAR* argv[]) { Base* pBase = new Derived(); delete pBase; Base* pBase2 = new Base(); // Error 1 error C2259: 'Base' : cannot instantiate abstract class } 
  1. Cuando desee que nadie pueda crear directamente el objeto de la clase Base, use el virtual virtual destructor virtual ~Base() = 0 . Por lo general, al menos se requiere una función virtual pura, tomemos virtual ~Base() = 0 , como esta función.

  2. Cuando no necesite lo anterior, solo necesita la destrucción segura del objeto Clase derivada

    Base * pBase = new Derived (); eliminar pBase; no se requiere un destructor virtual puro, solo el destructor virtual hará el trabajo.

Estás entrando en hipotéticas con estas respuestas, así que intentaré hacer una explicación más simple y más sencilla por cuestiones de claridad.

Las relaciones básicas del diseño orientado a objetos son dos: IS-A y HAS-A. No los inventé. Así es como se llaman.

IS-A indica que un objeto particular se identifica como el de la clase que está encima de él en una jerarquía de clases. Un objeto de plátano 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 una banana. No es reflexivo, sin embargo. No se 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 hay una relación de propiedad. Significa en C ++ que es un objeto miembro y, por lo tanto, la responsabilidad recae en la clase propietaria para deshacerse de él o deshacerse de 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 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 polymorphism, ya que se usa para decidir qué función ejecutar en el momento en que se llama en el progtwig en ejecución.

La palabra clave virtual es una directiva de comstackción para vincular funciones en un orden determinado si existe ambigüedad sobre la identidad de clase. Las funciones virtuales siempre están en clases principales (hasta donde yo sé) e indican al comstackdor 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 clase padre.

Una clase de fruta podría tener un color de función virtual () que devuelve “NINGUNO” de manera predeterminada. La función de color de clase Banana () devuelve “AMARILLO” o “MARRÓN”.

Pero si la función que toma un puntero de Fruit llama a color () en la clase Banana que se le envía, ¿qué función de color () se invoca? La función normalmente llamaría a Fruit :: color () para un objeto Fruit.

Eso sería el 99% del tiempo no fue lo que se pretendía. Pero si se declarara Fruit :: color () virtual, se llamaría Banana: color () para el objeto porque la función color () correcta se vincularía al puntero de Fruit en el momento de la llamada. El tiempo de ejecución comprobará a qué objeto apunta el puntero porque se marcó como 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 IS-A apunta a Fruit.

Entonces 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 el método de clase base nunca se llame. De hecho, no se puede llamar a una función virtual pura. Sin embargo, aún debe definirse. Debe existir una firma de función. Muchos codificadores realizan una implementación vacía {} para completar, pero el comstackdor generará uno internamente si no es así. En ese caso, cuando se llama a la función incluso si el puntero es Fruit, se llamará a Banana :: color () ya que es la única implementación de color () que hay.

Ahora la última pieza 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 quiera prohibir la creación de una instancia de clase base. Solo se pueden instanciar las subclases si el destructor de la clase base es puramente virtual. la convención es asignarlo a 0.

  virtual ~Fruit() = 0; // pure virtual Fruit::~Fruit(){} // destructor implementation 

Tienes que crear una implementación en este caso. El comstackdor sabe que esto es lo que estás haciendo y se asegura de que lo hagas bien, o se queja poderosamente de que no puede vincular todas las funciones que necesita comstackr. Los errores pueden ser confusos si no está en el camino correcto en cuanto a cómo está modelando su jerarquía de clases.

Por lo tanto, está prohibido en este caso crear instancias de Fruta, pero puede crear instancias de Banana.

Una llamada para eliminar el puntero de Fruit que apunta a una instancia de Banana llamará Banana :: ~ Banana () primero y luego llamará a Fuit :: ~ Fruit (), siempre. Porque no importa qué, cuando llamas a un destructor de subclase, el destructor de la clase base debe seguir.

¿Es un mal modelo? Es más complicado en la fase de diseño, sí, pero puede garantizar que se realice una vinculación correcta en el 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 pase solo punteros de clase exactos sin punteros generics ni ambiguos, entonces las funciones virtuales no son realmente necesarias. Pero si necesita flexibilidad en el tiempo de ejecución de los tipos (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á al color () con su propia función correcta.

Espero que esta explicación larga solidifique el concepto en lugar de confundir las cosas. Hay muchos buenos ejemplos que hay que mirar, y mirar lo suficiente y ejecutarlos y jugar con ellos y lo obtendrás.

Pidió 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 pueda lanzar el tipo error_base , pero los tipos de excepción error_oh_shucks y error_oh_blast tienen una funcionalidad idéntica y no quiero escribirlo dos veces. La complejidad de pImpl es necesaria para evitar exponer std::string a mis clientes, y el uso de std::auto_ptr necesita el constructor de copia.

El encabezado público contiene las especificaciones de excepción que estarán disponibles para que el cliente distinga los diferentes tipos de excepciones lanzadas por mi biblioteca:

 // error.h #include  #include  class exception_string; class error_base : public std::exception { public: error_base(const char* error_message); error_base(const error_base& other); virtual ~error_base() = 0; // Not directly usable virtual const char* what() const; private: std::auto_ptr error_message_; }; template class error : public error_base { public: error(const char* error_message) : error_base(error_message) {} error(const error& other) : error_base(other) {} ~error() {} }; // Neither should these classes be usable class error_oh_shucks { virtual ~error_oh_shucks() = 0; } class error_oh_blast { virtual ~error_oh_blast() = 0; } 

Y aquí está la implementación compartida:

 // error.cpp #include "error.h" #include "exception_string.h" error_base::error_base(const char* error_message) : error_message_(new exception_string(error_message)) {} error_base::error_base(const error_base& other) : error_message_(new exception_string(other.error_message_->get())) {} error_base::~error_base() {} const char* error_base::what() const { return error_message_->get(); } 

La clase exception_string, que se mantiene privada, oculta std :: string de mi interfaz pública:

 // exception_string.h #include  class exception_string { public: exception_string(const char* message) : message_(message) {} const char* get() const { return message_.c_str(); } private: std::string message_; }; 

Mi código arroja un error como:

 #include "error.h" throw error("That didn't work"); 

El uso de una plantilla para el error es un poco gratuito. Ahorra un poco de código a expensas de exigir a los clientes que capturen errores como:

 // client.cpp #include  try { } catch (const error&) { } catch (const error&) { } 

Tal vez haya otro REAL USE-CASE de destructor virtual puro que 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:

 class Printable { virtual void print() const = 0; // virtual destructor should be here, but not to confuse with another problem }; 

y algo así como:

 class Printer { void queDocument(unique_ptr doc); void printAll(); }; 

Simplemente, tenemos la interfaz Printable y un “contenedor” que contiene algo con esta interfaz. Creo que aquí está bastante claro por qué el método print() es puramente virtual. Podría tener algún cuerpo, pero en caso de que no haya una implementación predeterminada, el término virtual puro es una “implementación” ideal (= “debe ser proporcionado por una clase descendiente”).

Y ahora imagine exactamente lo mismo, excepto que no es para impresión sino para destrucción:

 class Destroyable { virtual ~Destroyable() = 0; }; 

Y también podría haber un contenedor similar:

 class PostponedDestructor { // Queues an object to be destroyed later. void queObjectForDestruction(unique_ptr obj); // Destroys all already queued objects. void destroyAll(); }; 

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 la print() “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 efectivamente y el comstackdor realmente genera un código vacío para él. Pero desde la perspectiva de un progtwigdor, la virtualidad pura aún significa: “No tengo ningún código predeterminado, debe ser proporcionado por clases derivadas”.

Creo que no es una gran idea, solo más explicación de que la virtualidad pura funciona de manera muy uniforme, también para los destructores.

Este es un tema de hace una década 🙂 Lea los últimos 5 párrafos del Artículo # 7 en el libro “Effective C ++” para más detalles, comienza con “Ocasionalmente puede ser conveniente dar a una clase un destructor virtual puro …”

1) Cuando desee requerir que las clases derivadas hagan la limpieza. Esto es raro.

2) No, pero quieres que sea virtual, sin embargo.

tenemos que hacer que destructor sea virtual porque, si no hacemos que el destructor sea virtual, el comstackdor solo destruirá el contenido de la clase base, n todas las clases derivadas permanecerán sin cambios, el comstackdor bacuse no llamará al destructor de ningún otro clase excepto la clase base.