web-dev-qa-db-fra.com

Pourquoi avons-nous besoin d'un destructeur virtuel pur en C ++?

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 mentionné que nous utilisions un destructeur virtuel pur lorsque nous voulions rendre une classe abstraite.

Mais nous pouvons faire une classe abstraite en rendant n'importe laquelle des fonctions membres pure virtuelle.

Donc mes questions sont

  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-ce une bonne pratique de rendre le destructeur également virtuel? Si oui, alors pourquoi?

148
Mark
  1. 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 elle ne permet pas d’avoir des effets pervers en autorisant 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, toute classe dérivée sera not ​​abstraite. Par conséquent, le destructeur virtuel pur dans la classe de base ne fera aucune différence pour les classes dérivées. Cela 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é).

Note: Le destructeur est la seule méthode qui, même si elle est virtuelle pure a avoir une implémentation pour instancier des 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.
};
111
Motti

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 aura n'importe quelle classe - elle est donc toujours présente en tant que candidat. De plus, rendre le destructeur pur virtuel (par opposition à juste 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 pur soit utilisé de manière cohérente pour indiquer qu'une classe est abstraite - si, pour la seule raison qu'il fournit un endroit cohérent, une personne lisant le code peut regarder si la classe est abstraite.

31
Braden

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

  • que 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 ne nécessite aucune autre dépêche virtuelle comportement pour les autres méthodes (peut-être existe-t-il pas d'autres méthodes? considérons un simple protégé " "ressource" qui nécessite un constructeur/destructeur/affectation 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 méthode).

Pour notre hypothétique ABC:

Vous garantissez qu’elle ne peut pas être instanciée (même à l’intérieur de la classe elle-même; c’est pourquoi les constructeurs privés peuvent ne pas suffire), vous obtenez le comportement virtuel souhaité pour le destructeur, et vous n’aurez pas à rechercher ni à baliser une autre méthode pas besoin de dispatch virtuel en tant que "virtuel".

18
leander

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 elle ne permet pas d’avoir des effets pervers en autorisant 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 mentionnées par Scott Meyers dans son livre "More Effective C++", élément 33 "Abstraction des classes non-feuille", il est préférable de créer une classe abstraite myAbstractClass dont héritent myClassA et myClassB. Cela fournit une meilleure abstraction et évite certains problèmes liés, par exemple, aux copies d'objet.

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é d'un 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 é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; 
};
8
Laurent Michel

Si vous souhaitez arrêter l'instanciation d'une 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.

4
sukumar

Ici, je veux dire quand nous avons besoin de destructeur virtuel et quand 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, en tant que cette fonction.

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

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

2
Anil8753

Vous entrez dans des hypothèses avec ces réponses, alors je vais essayer de rendre une explication plus simple, plus concrète pour des raisons 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 comme ça qu'ils s'appellent.

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 substituer une classe de base à 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é. En C++, cela signifie 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.

Ces deux concepts sont plus faciles à comprendre dans les langages à héritage unique que dans un modèle à héritage multiple tel que 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 chose au moment de l’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 en premier avec la fonction sous-classe et la fonction classe ensuite.

Une classe Fruit peut avoir une fonction virtuelle color () qui renvoie "NONE" par défaut. La fonction color () de classe banane renvoie "JAUNE" ou "MARRON".

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 ne serait pas 99% du temps 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 sur quel objet le pointeur pointe 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.

Alors maintenant, l’idée d’une "fonction virtuelle pure" se pose. 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 veille à le faire correctement, ou se plaint fort de ne pas pouvoir créer de lien vers toutes les fonctions nécessaires à la compilation. Les erreurs peuvent être source de confusion si vous n'êtes pas sur la bonne voie pour modéliser votre hiérarchie de classes.

Dans ce cas, il vous est donc interdit de créer des instances de Fruit, mais autorisé de créer des instances de Banana.

Un appel à supprimer du pointeur Fruit qui pointe vers une instance de Banana appellera d’abord Banana :: ~ Banana (), puis Fuit :: ~ Fruit (), toujours. En tout état de cause, lorsque vous appelez un destructeur de sous-classe, le destructeur de classe de base doit suivre.

Est-ce un mauvais modèle? C'est plus compliqué dans la phase de conception, oui, mais cela peut garantir qu'une liaison correcte est effectuée au moment de l'exécution et qu'une fonction de sous-classe est exécutée lorsqu'il y a une ambiguïté quant à savoir exactement quelle sous-classe est utilisée.

Si vous écrivez en C++ de manière à ne transmettre que des pointeurs de classe exacts sans pointeurs génériques ni ambigus, les fonctions virtuelles ne sont pas vraiment nécessaires. Mais si vous avez besoin d’une souplesse d’exécution des types (comme dans les fonctions Apple Banana Orange ==> Fruit)), il est plus facile et plus polyvalent de disposer d’un code moins redondant. chaque type de fruit, et vous savez que chaque fruit répondra à la couleur () avec sa propre fonction correcte.

J'espère que cette longue explication solidifie le concept plutôt que de confondre les choses. Il y a beaucoup de bons exemples à regarder, et regarder suffisamment de choses et réellement les exécuter et les déconner avec et vous l'obtiendrez.

2
Chris Reid

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 jeter le error_base type, mais les types d'exception error_oh_shucks et error_oh_blast ont des fonctionnalités identiques et je ne veux pas les é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, cache std :: string 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 renvoie ensuite une erreur en tant que:

#include "error.h"

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

L'utilisation d'un template pour error est un peu gratuite. Cela économise un peu de code au détriment d’obliger les clients à détecter les erreurs telles que:

// client.cpp

#include <error.h>

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

Peut-être y at-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 pas encore 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 Printable et un "conteneur" contenant quoi que ce soit avec cette interface. Je pense ici qu’il est assez clair pourquoi la méthode print() est virtuelle. Il pourrait avoir un corps quelconque, mais en l'absence d'implémentation par défaut, pure virtual est une "implémentation" idéale (= "doit être fournie 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 aussi, il pourrait 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é de mon application réelle. La seule différence ici est que la méthode "spéciale" (destructeur) a été utilisée à la place de "normale" print(). Mais la raison pour laquelle elle est virtuelle est toujours la même: il n’ya 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 pas de 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 juste une explication supplémentaire sur le fait que la virtualité pure fonctionne de manière vraiment uniforme - également pour les destructeurs.

0
Jarek C

C'est un sujet vieux de dix ans :) Lisez les 5 derniers paragraphes de l'item 7 sur le livre "Effective C++" pour plus de détails, commence par "Il peut parfois être pratique de donner à une classe un pur destructeur virtuel ...."

0
J-Q