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


154

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

  1. ¿Cuándo hacemos realmente que un destructor sea virtual puro? ¿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í ... entonces ¿por qué?



14
@ Daniel- Los enlaces mencionados no responden mi pregunta. Responde por qué un destructor virtual puro debería tener una definición. Mi pregunta es por qué necesitamos un destructor virtual puro.
Mark

Estaba tratando de averiguar la razón, pero ya hiciste la pregunta aquí.
nsivakr

Respuestas:


119
  1. 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.

  2. 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).

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.
};

13
"sí, las funciones virtuales puras pueden tener implementaciones" Entonces no es puramente virtual.
GManNickG

2
Si desea hacer un resumen de clase, ¿no sería más simple proteger a todos los constructores?
bdonlan

78
@GMan, te equivocas, ser virtual puro significa que las clases derivadas deben anular este método, esto es ortogonal para tener una implementación. Mira mi código y comenta foof::barsi quieres verlo por ti mismo.
Motti el

15
@GMan: el C ++ FAQ lite dice "Tenga en cuenta que es posible proporcionar una definición para una función virtual pura, pero esto generalmente confunde a los novatos y es mejor evitarlo hasta más tarde". parashift.com/c++-faq-lite/abcs.html#faq-22.4 Wikipedia (ese bastión de corrección) también dice lo mismo. Creo que el estándar ISO / IEC usa una terminología similar (desafortunadamente mi copia está funcionando en este momento) ... Estoy de acuerdo en que es confuso, y generalmente no uso el término sin aclarar cuando proporciono una definición, especialmente alrededor de los nuevos programadores ...
leander

9
@ Motti: Lo que es interesante aquí y proporciona más confusión es que el destructor virtual puro NO necesita ser anulado explícitamente en la clase derivada (y instanciada). En tal caso, se utiliza la definición implícita :)
kappa

33

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.


1
pero todavía por qué proporcionar la implementación del destructor virtaul puro. Lo que podría salir mal es que convierto un destructor en virtual puro y no proporciona su implementación. Supongo que solo se declaran punteros de clases base y, por lo tanto, nunca se llama al destructor de la clase abstracta.
Krishna Oza

44
@Surfing: porque un destructor de una clase derivada llama implícitamente al destructor de su clase base, incluso si ese destructor es virtual puro. Por lo tanto, si no hay una implementación, se producirá un comportamiento indefinido.
a.peganz

19

Si desea crear una clase base abstracta:

  • eso no puede ser instanciado (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 eliminarlos)
  • pero no se necesita ningún otro despacho virtual de la conducta de otros métodos (tal vez no son ningún otro método? considerar un simple contenedor protegido "recurso" que necesita 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 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".


8

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:

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.

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.

struct IParams
{
    IParams(const ModelConfiguration& aModelConf);
    virtual ~IParams() = 0;

    void setParameter(const N_Configuration::Parameter& aParam);

    std::map<std::string, std::string> 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; 
};

1
Me gusta este uso, pero otra forma de "imponer" la herencia es declarando que el constructor IParamestá protegido, como se señaló en otro comentario.
rwols

4

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.


3

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 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, tomemos virtual ~Base() = 0como esta función.

  2. 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.


2

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.

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

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.


1

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 ..."


0

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_basetipo, pero los tipos de excepción error_oh_shucksy error_oh_blasttener una funcionalidad idéntica y no quiero escribirlo dos veces. La complejidad de pImpl es necesaria para evitar exponerme std::stringa mis clientes, y el uso de std::auto_ptrnecesita 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:

// error.h

#include <exception>
#include <memory>

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<exception_string> error_message_;
};

template<class error_type>
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, mantenida en privado, oculta std :: string de mi interfaz pública:

// exception_string.h

#include <string>

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<error_oh_shucks>("That didn't work");

El uso de una plantilla para errores un poco gratuito. Ahorra un poco de código a expensas de requerir que los clientes detecten errores como:

// client.cpp

#include <error.h>

try {
} catch (const error<error_oh_shucks>&) {
} catch (const error<error_oh_blast>&) {
}

0

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:

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

y algo como:

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

Simplemente, tenemos una interfaz Printabley algún "contenedor" que contiene cualquier cosa con esta interfaz. Creo que aquí está bastante claro por qué el print()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:

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<Destroyable> 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 "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.


-2

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

2) No, pero quieres que sea virtual.


-2

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.


-1: La pregunta no es por qué un destructor debería ser virtual.
Trovador

Además, en ciertas situaciones los destructores no tienen que ser virtuales para lograr la destrucción correcta. Los destructores virtuales solo son necesarios cuando terminas llamando deletea un puntero a la clase base cuando de hecho apunta a su derivada.
CygnusX1

Estás 100% correcto. Esta es y ha sido en el pasado una de las fuentes número uno de fugas y bloqueos en los programas de C ++, en tercer lugar solo para tratar de hacer cosas con punteros nulos y exceder los límites de las matrices. Se llamará a un destructor de clase base no virtual en un puntero genérico, omitiendo por completo el destructor de la subclase si no está marcado como virtual. Si hay objetos creados dinámicamente que pertenecen a la subclase, el destructor base no los recuperará en una llamada a eliminar. Estás avanzando bien, entonces BLUURRK! (difícil de encontrar, también.)
Chris Reid
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.