Question

Je comprends la nécessité d’un destructeur virtuel. Mais pourquoi avons-nous besoin d'un destructeur virtuel pur? Dans l'un des articles sur C ++, l'auteur a indiqué que nous utilisions un destructeur virtuel pur lorsque nous voulions rendre une classe abstraite.

Mais nous pouvons rendre une classe abstraite en rendant n'importe quelle fonction membre pure-virtuelle.

Mes questions sont donc

  1. Quand fait-on vraiment un destructeur purement virtuel? Quelqu'un peut-il donner un bon exemple en temps réel?

  2. Lorsque nous créons des classes abstraites, est-il recommandé de rendre le destructeur également virtuel? Si oui, alors pourquoi?

Était-ce utile?

La solution

  1. Probablement la vraie raison pour laquelle les destructeurs virtuels purs sont autorisés est que leur interdiction reviendrait à ajouter une autre règle au langage. Cette règle n'est pas nécessaire car aucun mal ne peut nuire à l'autorisation d'un destructeur virtuel pur.

  2. Non, un simple vieux virtuel suffit.

Si vous créez un objet avec des implémentations par défaut pour ses méthodes virtuelles et souhaitez le rendre abstrait sans forcer personne à remplacer une méthode spécifique , vous pouvez rendre le destructeur pur virtuel. Je n'y vois pas grand chose mais c'est possible.

Notez que puisque le compilateur générera un destructeur implicite pour les classes dérivées, si son auteur ne le fait pas, les classes dérivées ne seront pas abstraites. Par conséquent, le destructeur virtuel pur dans la classe de base ne fera aucune différence pour les classes dérivées. Il ne fera que rendre la classe de base abstraite (merci pour le commentaire de @kappa ).

On peut également supposer que chaque classe dérivée aurait probablement besoin de code de nettoyage spécifique et d’utiliser le destructeur virtuel pur comme rappel pour en écrire un, mais cela semble artificiel (et non appliqué).

Remarque: le destructeur est la seule méthode qui, même s'il est pur virtuel, doit disposer d'une implémentation permettant d'instancier les classes dérivées. (oui, les fonctions virtuelles pures peuvent avoir des implémentations).

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

Autres conseils

Tout ce dont vous avez besoin pour une classe abstraite est au moins une fonction virtuelle pure. Toute fonction fera l'affaire; mais il se trouve que le destructeur est quelque chose que n'importe quelle classe aura & # 8212; elle est donc toujours présente en tant que candidat. De plus, rendre le destructeur pur virtuel (par opposition à simplement virtuel) n'a pas d'effets secondaires comportementaux autres que de rendre la classe abstraite. En tant que tels, de nombreux guides de style recommandent que le purificateur virtuel soit utilisé de manière cohérente pour indiquer qu’une classe est abstraite si, pour la seule raison qui l’autorise, elle fournit un emplacement cohérent à la lecture du code. résumé.

Si vous souhaitez créer une classe de base abstraite:

  • cela ne peut pas être instancié (oui, cela est redondant avec le terme "abstrait"!)
  • mais nécessite un comportement de destructeur virtuel (vous avez l'intention de déplacer les pointeurs vers l'ABC plutôt que les pointeurs vers les types dérivés et de les supprimer)
  • mais n'a besoin d'aucun autre comportement d'envoi virtuel pour les autres méthodes (peut-être n'y a-t-il pas d'autres méthodes? Envisagez un simple conteneur "ressource" protégé qui nécessite des constructeurs / destructor / assign mais pas grand chose d’autre)

... il est plus facile de rendre la classe abstraite en rendant le destructeur pur virtuel et en lui fournissant une définition (corps de la méthode).

Pour notre ABC hypothétique:

Vous garantissez qu'il ne peut pas être instancié (même à l'intérieur de la classe elle-même, c'est pourquoi les constructeurs privés ne suffisent peut-être pas), vous obtenez le comportement virtuel souhaité pour le destructeur, et vous n'avez pas à en rechercher ni en marquer un autre. méthode qui n’a pas besoin d’envoi virtuel en tant que "virtuel".

D'après les réponses que j'ai lues à votre question, je ne pouvais pas déduire une bonne raison d'utiliser réellement un destructeur virtuel. Par exemple, la raison suivante ne me convainc pas du tout:

  

La vraie raison pour laquelle les destructeurs virtuels purs sont autorisés est probablement que leur interdiction reviendrait à ajouter une autre règle au langage. Cette règle n'est pas nécessaire, car aucun destructeur ne peut nuire à l'autorisation d'un destructeur virtuel pur.

À mon avis, les destructeurs virtuels purs peuvent être utiles. Par exemple, supposons que votre code comporte deux classes myClassA et myClassB, et que myClassB hérite de myClassA. Pour les raisons évoquées par Scott Meyers dans son livre "More Effective C ++", Item 33 "Rendre abstraites les classes non-feuilles", il est préférable de créer une classe abstraite myAbstractClass dont myClassA et myClassB héritent. Ceci fournit une meilleure abstraction et évite certains problèmes liés, par exemple, aux copies d’objets.

Dans le processus d'abstraction (création de la classe myAbstractClass), il se peut qu'aucune méthode de myClassA ou myClassB ne soit un bon candidat pour être une méthode virtuelle pure (condition préalable à l'abstraction de myAbstractClass). Dans ce cas, vous définissez le destructeur pur virtuel de la classe abstraite.

Ci-après un exemple concret tiré du code que j'ai moi-même écrit. J'ai deux classes, Numerics / PhysicsParams, qui partagent des propriétés communes. Je les ai donc laissés hériter de la classe abstraite IParams. Dans ce cas, je n'avais absolument aucune méthode en main qui puisse être purement virtuelle. La méthode setParameter, par exemple, doit avoir le même corps pour chaque sous-classe. Le seul choix que j’ai eu à faire était de rendre le destructeur d’IParams purement virtuel.

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

Si vous souhaitez arrêter l'instanciation de la classe de base sans apporter de modification à votre classe dérivée déjà implémentée et testée, vous implémentez un destructeur virtuel pur dans votre classe de base.

Je souhaite indiquer ici quand nous avons besoin de destructeur virtuel et lorsque nous avons besoin de destructeur virtuel pur

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. Si vous souhaitez que personne ne puisse créer l'objet de la classe de base directement, utilisez le destructeur virtuel pur virtual ~ Base () = 0 . Habituellement, au moins une fonction virtuelle pure est requise, prenons virtual ~ Base () = 0 , comme cette fonction.

  2. Lorsque vous n'avez pas besoin de ce qui précède, vous avez uniquement besoin de la destruction en toute sécurité de l'objet de classe Derived

    .

    Base * pBase = new Derived (); supprimer pBase; Un destructeur virtuel pur n’est pas requis, seul un destructeur virtuel fera le travail.

Vous entrez dans des hypothèses avec ces réponses, je vais donc essayer de rendre plus simple une explication plus réaliste, par souci de clarté.

Les relations de base de la conception orientée objet sont deux:  IS-A et HAS-A. Je ne les ai pas inventés. C’est ainsi qu’on les appelle.

IS-A indique qu'un objet particulier s'identifie comme appartenant à la classe située au-dessus de lui dans une hiérarchie de classes. Un objet banane est un objet fruit s'il s'agit d'une sous-classe de la classe fruit. Cela signifie que partout où une classe de fruits peut être utilisée, une banane peut être utilisée. Ce n'est pas réflexif, cependant. Vous ne pouvez pas remplacer une classe de base par une classe spécifique si cette classe spécifique est appelée.

Has-a indique qu'un objet fait partie d'une classe composite et qu'il existe une relation de propriété. Cela signifie en C ++ qu’il s’agit d’un objet membre et qu’il incombe donc à la classe propriétaire d’en disposer ou de céder la propriété avant de se détruire elle-même.

Ces deux concepts sont plus faciles à comprendre dans les langages à héritage unique que dans un modèle à héritage multiple comme c ++, mais les règles sont essentiellement les mêmes. La complication survient lorsque l'identité de la classe est ambiguë, par exemple en passant un pointeur de classe Banana dans une fonction prenant un pointeur de classe Fruit.

Les fonctions virtuelles sont d’abord une fonction d’exécution. Cela fait partie du polymorphisme en ce qu'il est utilisé pour décider de la fonction à exécuter au moment où elle est appelée dans le programme en cours d'exécution.

Le mot-clé virtual est une directive de compilation permettant de lier des fonctions dans un certain ordre en cas d'ambiguïté sur l'identité de la classe. Les fonctions virtuelles sont toujours dans les classes parentes (pour autant que je sache) et indiquent au compilateur que la liaison des fonctions membres à leurs noms doit avoir lieu avec la fonction de sous-classe en premier et la fonction de classe parent après.

Une classe de fruits peut avoir une fonction virtuelle color () qui renvoie "NONE". par défaut. La fonction color () de la classe Banana renvoie "JAUNE". ou "BROWN".

Mais si la fonction prenant un pointeur de Fruit appelle color () sur la classe Banana qui lui est envoyée, quelle fonction color () est invoquée? La fonction appelle normalement Fruit :: color () pour un objet Fruit.

Ce serait 99% du temps ne serait pas ce qui était prévu. Mais si Fruit :: color () était déclaré virtuel, Banana: color () serait appelé pour l'objet car la fonction color () correcte serait liée au pointeur Fruit au moment de l'appel. Le moteur d’exécution vérifiera l’objet pointé par le pointeur car il a été marqué comme virtuel dans la définition de la classe Fruit.

Cela diffère de la redéfinition d'une fonction dans une sous-classe. Dans ce cas le pointeur Fruit appellera Fruit :: color () si tout ce qu'il sait, c'est qu'il est un pointeur sur Fruit.

Passons maintenant à l'idée d'une "fonction virtuelle pure". se lève. C'est une phrase plutôt malheureuse, car la pureté n'y est pour rien. Cela signifie qu'il est prévu que la méthode de la classe de base ne soit jamais appelée. En effet, une fonction virtuelle pure ne peut pas être appelée. Il doit encore être défini, cependant. Une signature de fonction doit exister. De nombreux codeurs font une implémentation vide {} pour être complet, mais le compilateur en générera une en interne sinon. Dans ce cas, lorsque la fonction est appelée même si le pointeur se trouve sur Fruit, Banana :: color () sera appelé car il s’agit de la seule implémentation de color ().

Maintenant la dernière pièce du puzzle: les constructeurs et les destructeurs.

Les constructeurs virtuels purs sont illégaux, complètement. C'est juste sorti.

Mais les destructeurs virtuels purs fonctionnent dans le cas où vous souhaitez interdire la création d'une instance de classe de base. Seules les sous-classes peuvent être instanciées si le destructeur de la classe de base est virtuel. la convention est de l'attribuer à 0.

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

Vous devez créer une implémentation dans ce cas. Le compilateur sait que c'est ce que vous faites et fait sur

Vous avez demandé un exemple et je pense que ce qui suit fournit une raison pour un destructeur virtuel pur. Je suis impatient de savoir s’il s’agit d’une bonne raison ...

Je ne veux pas que quiconque puisse lancer le type error_base , mais les types d'exception error_oh_shucks et error_oh_blast ont une fonctionnalité identique et I ne veux pas l'écrire deux fois. La complexité de pImpl est nécessaire pour éviter d'exposer std :: string à mes clients, et l'utilisation de std :: auto_ptr nécessite le constructeur de copie.

L'en-tête public contient les spécifications d'exception qui seront disponibles pour le client afin de distinguer les différents types d'exceptions levées par ma bibliothèque:

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

Et voici l'implémentation partagée:

// 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 classe exception_string, maintenue privée, masque std :: string à partir de mon interface publique:

// 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_;
};

Mon code génère ensuite une erreur en tant que:

#include "error.h"

throw error<error_oh_shucks>("That didn't work");

L'utilisation d'un modèle pour erreur est un peu gratuite. Il enregistre un peu de code au détriment de demander aux clients de détecter les erreurs en tant que:

// client.cpp

#include <error.h>

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

Peut-être existe-t-il un autre REAL USE-CASE de destructeur virtuel pur que je ne vois pas dans les autres réponses:)

Au début, je suis tout à fait d’accord avec la réponse marquée: c’est parce qu’interdire les destructeurs virtuels purs aurait besoin d’une règle supplémentaire dans la spécification du langage. Mais ce n’est toujours pas le cas d’utilisation que Mark appelle:)

Imaginons d'abord ceci:

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

et quelque chose comme:

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

Simplement, nous avons l'interface imprimable et certains "conteneur". rien tenir avec cette interface. Je pense qu'ici, il est assez clair pourquoi la méthode print () est virtuelle. Il pourrait avoir un corps quelconque, mais au cas où il n’y aurait pas d’implémentation par défaut, le virtuel pur serait une "implémentation" idéale. (= "doit être fourni par une classe descendante").

Et maintenant, imaginez exactement la même chose, sauf que ce n'est pas pour l'impression mais pour la destruction:

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

Et il pourrait également y avoir un conteneur similaire:

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

C'est un cas d'utilisation simplifié à partir de mon application réelle. La seule différence ici est que " special " méthode (destructeur) a été utilisé à la place de "normal". print () . Mais la raison pour laquelle il s'agit de virtuel pur est toujours la même: il n'y a pas de code par défaut pour la méthode. Un peu déroutant pourrait être le fait qu'il DOIT y avoir un destructeur de manière efficace et que le compilateur génère un code vide pour celui-ci. Mais du point de vue du programmeur, la virtualité pure signifie toujours: "Je n’ai aucun code par défaut, il doit être fourni par des classes dérivées."

Je pense que ce n’est pas une grande idée ici, mais une explication supplémentaire: la virtualité pure fonctionne de manière vraiment uniforme, y compris pour les destructeurs.

C'est un sujet vieux d'une décennie :) Lisez les 5 derniers paragraphes de l’article 7 sur " Effective C ++ " livre pour plus de détails, commence par "Parfois, il peut être pratique de donner à une classe un pur destructeur virtuel ...."

1) Lorsque vous souhaitez exiger le nettoyage des classes dérivées. C'est rare.

2) Non, mais vous voulez que ce soit virtuel, cependant.

nous devons rendre destructeur virtuel le fait que, si nous ne rendons pas le destructeur virtuel, le compilateur ne fera que détruire le contenu de la classe de base, n toutes les classes dérivées resteront inchangées, le compilateur bacuse n'appellera pas le destructeur de toute autre classe sauf la classe de base.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top