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


154

Je comprends la nécessité d'un destructeur virtuel. Mais pourquoi avons-nous besoin d'un pur destructeur virtuel? Dans l'un des articles C ++, l'auteur a mentionné que nous utilisons un destructeur virtuel pur lorsque nous voulons créer un résumé de classe.

Mais nous pouvons créer une classe abstraite en faisant de l'une des fonctions membres une fonction purement 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 purement virtuel? Si oui ... alors pourquoi?



14
@ Daniel- Les liens mentionnés ne répondent pas à ma question. Cela explique pourquoi un destructeur virtuel pur devrait avoir une définition. Ma question est pourquoi nous avons besoin d'un pur destructeur virtuel.
Mark

J'essayais de trouver la raison, mais vous avez déjà posé la question ici.
nsivakr

Réponses:


119
  1. La vraie raison pour laquelle les destructeurs virtuels purs sont autorisés est probablement que les interdire signifierait ajouter une autre règle au langage et il n'y a pas besoin de cette règle car aucun effet néfaste ne peut provenir de l'autorisation d'un destructeur virtuel pur.

  2. Non, le simple vieux virtuel suffit.

Si vous créez un objet avec des implémentations par défaut pour ses méthodes virtuelles et que vous souhaitez le rendre abstrait sans forcer personne à remplacer une méthode spécifique , vous pouvez rendre le destructeur purement 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 l'auteur de la classe ne le fait pas, les classes dérivées ne seront pas abstraites. Par conséquent, avoir 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 d'avoir un code de nettoyage spécifique et d'utiliser le destructeur virtuel pur comme un 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 purement virtuel, doit 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.
};

13
"oui les fonctions virtuelles pures peuvent avoir des implémentations" Alors ce n'est pas du pur virtuel.
GManNickG

2
Si vous voulez créer une classe abstraite, ne serait-il pas plus simple de simplement protéger tous les constructeurs?
bdonlan

78
@GMan, vous vous trompez, étant purement virtuel signifie que les classes dérivées doivent remplacer cette méthode, c'est orthogonal à avoir une implémentation. Consultez mon code et commentez foof::barsi vous voulez voir par vous-même.
Motti

15
@GMan: la FAQ C ++ lite dit "Notez qu'il est possible de fournir une définition pour une fonction virtuelle pure, mais cela déroute généralement les novices et il vaut mieux l'éviter jusqu'à plus tard." parashift.com/c++-faq-lite/abcs.html#faq-22.4 Wikipedia (ce bastion de l'exactitude) dit également de même. Je pense que la norme ISO / CEI utilise une terminologie similaire (malheureusement ma copie est en cours de réalisation pour le moment) ... Je reconnais que c'est déroutant, et je n'utilise généralement pas le terme sans clarification lorsque je donne une définition, en particulier autour de nouveaux programmeurs ...
maigre

9
@Motti: Ce qui est intéressant ici et fournit plus de confusion, c'est que le destructeur virtuel pur n'a PAS besoin d'être explicitement remplacé dans la classe dérivée (et instanciée). Dans un tel cas, la définition implicite est utilisée :)
kappa

33

Tout ce dont vous avez besoin pour une classe abstraite est au moins une fonction virtuelle pure. N'importe quelle fonction fera l'affaire; mais en l'occurrence, le destructeur est quelque chose que n'importe quelle classe aura - donc il est toujours là comme candidat. De plus, rendre le destructeur purement virtuel (par opposition à juste virtuel) n'a aucun effet secondaire comportemental autre que de rendre la classe abstraite. En tant que tel, de nombreux guides de style recommandent que le pur destructeur virtuel soit utilisé de manière cohérente pour indiquer qu'une classe est abstraite - si pour aucune autre raison que celle-ci fournit un endroit cohérent que quelqu'un lisant le code peut regarder pour voir si la classe est abstraite.


1
mais encore pourquoi fournir la mise en œuvre du pur destructeur virtaul. Qu'est-ce qui pourrait mal tourner? Je fais un destructeur purement virtuel et ne fournit pas son implémentation. Je suppose que seuls les pointeurs de classes de base sont déclarés et que le destructeur de la classe abstraite n'est donc jamais appelé.
Krishna Oza

4
@Surfing: parce qu'un destructeur d'une classe dérivée appelle implicitement le destructeur de sa classe de base, même si ce destructeur est purement virtuel. Donc, s'il n'y a pas d'implémentation pour cela, un comportement non défini va se produire.
a.peganz

19

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

  • qui ne peut pas être instancié (oui, c'est redondant avec le terme "abstrait"!)
  • mais nécessite un comportement de destructeur virtuel (vous avez l'intention de transporter des pointeurs vers l'ABC plutôt que des pointeurs vers les types dérivés, et de les supprimer)
  • mais n'a pas besoin d'un autre comportement de répartition virtuelle pour d'autres méthodes (peut-être qu'il n'y a pas d'autres méthodes? considérez un simple conteneur de «ressources» protégé qui a besoin d'un constructeur / destructeur / affectation mais pas grand chose d'autre)

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

Pour notre ABC hypothétique:

Vous garantissez qu'il ne peut pas être instancié (même interne à la classe elle-même, c'est pourquoi les constructeurs privés peuvent ne pas suffire), vous obtenez le comportement virtuel que vous souhaitez pour le destructeur, et vous n'avez pas à trouver et baliser une autre méthode qui ne 'pas besoin de répartition virtuelle comme "virtuelle".


8

D'après les réponses que j'ai lues à votre question, je n'ai pas pu déduire une bonne raison d'utiliser réellement un destructeur virtuel pur. 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 les interdire signifierait ajouter une autre règle au langage et il n'y a pas besoin de cette règle car aucun effet néfaste ne peut provenir de 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 "Making non-leaf classes abstract", il est préférable de créer une classe abstraite myAbstractClass dont myClassA et myClassB héritent. Cela fournit une meilleure abstraction et évite que certains problèmes surviennent avec, par exemple, les copies d'objets.

Dans le processus d'abstraction (de 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 (ce qui est une condition préalable pour que myAbstractClass soit abstraite). Dans ce cas, vous définissez le destructeur purement virtuel de la classe abstraite.

Ci-après un exemple concret 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 laisse donc hériter de la classe abstraite IParams. Dans ce cas, je n'avais absolument aucune méthode à portée de main qui pourrait ê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; 
};

1
J'aime cet usage, mais une autre façon d '"appliquer" l'héritage est de déclarer que le constructeur de IParamest protégé, comme cela a été noté dans un autre commentaire.
rwols

4

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


3

Ici, je veux dire quand nous avons besoin d' un destructeur virtuel et quand nous avons besoin d' un 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. Lorsque vous souhaitez que personne ne puisse créer directement l'objet de la classe de base, utilisez un 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 la chose ci-dessus, vous avez seulement besoin de la destruction sûre de l'objet de classe dérivée

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


2

Vous entrez dans des hypothèses avec ces réponses, alors je vais essayer de faire une explication plus simple et plus terre-à-terre par souci de clarté.

Les relations de base de la conception orientée objet sont au nombre de deux: IS-A et HAS-A. Je n'ai pas inventé ceux-là. C'est comme ça qu'on les appelle.

IS-A indique qu'un objet particulier s'identifie comme faisant partie de la classe qui se trouve 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 cependant pas réflexif. Vous ne pouvez pas substituer une classe de base à une classe spécifique si cette classe spécifique est appelée.

Has-a a indiqué 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'en tant que tel, il incombe à la classe propriétaire de s'en débarrasser ou de transférer la propriété avant de se détruire.

Ces deux concepts sont plus faciles à réaliser dans les langages à héritage unique que dans un modèle d'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ë, comme le passage d'un pointeur de classe Banana dans une fonction qui prend un pointeur de classe Fruit.

Les fonctions virtuelles sont, tout d'abord, une chose au moment de l'exécution. Il fait partie du polymorphisme en ce qu'il est utilisé pour décider quelle 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 du compilateur pour lier les fonctions dans un certain ordre s'il y a 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 parente après.

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

Mais si la fonction prenant un pointeur Fruit appelle color () sur la classe Banana qui lui est envoyée - quelle fonction color () est appelé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é virtual alors 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.

Ceci est différent du remplacement 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 IS-A vers Fruit.

Alors maintenant, l'idée d'une "fonction virtuelle pure" surgit. C'est une phrase plutôt malheureuse car la pureté n'a rien à voir avec cela. Cela signifie qu'il est prévu que la méthode de classe de base ne soit jamais appelée. En effet, une fonction virtuelle pure ne peut pas être appelée. Cependant, il doit encore être défini. Une signature de fonction doit exister. De nombreux codeurs font une implémentation vide {} par souci d'exhaustivité, 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 est sur Fruit, Banana :: color () sera appelée car c'est la seule implémentation de color () qui existe.

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

Les constructeurs virtuels purs sont complètement illégaux. 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 purement 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 s'assure que vous le faites correctement, ou il se plaint fortement de ne pas pouvoir se lier à toutes les fonctions dont il a besoin pour compiler. Les erreurs peuvent être déroutantes si vous n'êtes pas sur la bonne voie quant à la façon dont vous modélisez votre hiérarchie de classes.

Il vous est donc interdit dans ce cas de créer des instances de Fruit, mais autorisé à créer des instances de Banana.

Un appel à la suppression du pointeur Fruit qui pointe vers une instance de Banana appellera d'abord Banana :: ~ Banana () puis appellera Fuit :: ~ Fruit (), toujours. Parce que quoi qu'il arrive, 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 que la liaison correcte est effectuée au moment de l'exécution et qu'une fonction de sous-classe est exécutée là où il y a une ambiguïté quant à la sous-classe exacte à laquelle on accède.

Si vous écrivez C ++ de façon à ne transmettre que des pointeurs de classe exacts sans pointeurs génériques ou ambigus, alors les fonctions virtuelles ne sont pas vraiment nécessaires. Mais si vous avez besoin d'une flexibilité d'exécution des types (comme dans Apple Banana Orange ==> Fruit), les fonctions deviennent plus faciles et plus polyvalentes avec un code moins redondant. Vous n'avez plus besoin d'écrire une fonction pour chaque type de fruit, et vous savez que chaque fruit répondra à color () 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 là-bas à regarder, à regarder suffisamment et à les exécuter et à les manipuler et vous les obtiendrez.


1

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


0

Vous avez demandé un exemple, et je pense que ce qui suit fournit une raison pour un destructeur virtuel pur. J'attends avec impatience les réponses pour savoir si c'est un bon raison ...

Je ne veux pas que quelqu'un soit capable de jeter le error_basegenre, mais les types d'exception error_oh_shuckset error_oh_blastont des fonctionnalités identiques et je ne veux pas écrire deux fois. La complexité pImpl est nécessaire pour éviter d'exposer std::stringà mes clients, et l'utilisation destd::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 pour distinguer 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, gardée privée, masque 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 alors une erreur comme:

#include "error.h"

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

L'utilisation d'un template pour errorest un peu gratuite. Cela permet d'économiser un peu de code au détriment d'exiger des clients qu'ils détectent les erreurs comme:

// client.cpp

#include <error.h>

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

0

Peut-être qu'il y a un autre CAS D'UTILISATION RÉEL du destructeur virtuel pur que je ne peux pas voir dans d'autres réponses :)

Au début, je suis complètement d'accord avec la réponse marquée: c'est parce que l'interdiction du destructeur virtuel pur nécessiterait une règle supplémentaire dans la spécification du langage. Mais ce n'est toujours pas le cas d'utilisation que Mark appelle :)

Imaginez 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 une interface Printableet un "conteneur" contenant n'importe quoi avec cette interface. Je pense qu'ici il est assez clair pourquoiprint() méthode est purement virtuelle. Il pourrait avoir un certain corps mais au cas où il n'y aurait pas d'implémentation par défaut, le pur virtuel 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 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é de ma vraie application. La seule différence ici est que la méthode "spéciale" (destructeur) a été utilisée au lieu de "normal" print(). Mais la raison pour laquelle il s'agit d'un pur virtuel 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 effectivement un destructeur et que le compilateur génère en fait un code vide pour cela. Mais du point de vue d'un 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, juste une explication supplémentaire sur le fait que la virtualité pure fonctionne vraiment uniformément - également pour les destructeurs.


-2

1) Lorsque vous souhaitez demander aux classes dérivées d'effectuer le nettoyage. C'est rare.

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


-2

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


-1: La question n'est pas de savoir pourquoi un destructeur devrait être virtuel.
Troubadour

De plus, dans certaines situations, les destructeurs n'ont pas besoin d'être virtuels pour réaliser une destruction correcte. Les destructeurs virtuels ne sont nécessaires que lorsque vous finissez par appeler deleteun pointeur vers la classe de base alors qu'en fait il pointe vers son dérivé.
CygnusX1

Vous avez raison à 100%. C'est et a été dans le passé l'une des principales sources de fuites et de plantages dans les programmes C ++, la troisième uniquement pour essayer de faire des choses avec des pointeurs nuls et dépasser les limites des tableaux. Un destructeur de classe de base non virtuelle sera appelé sur un pointeur générique, en contournant entièrement le destructeur de sous-classe s'il n'est pas marqué comme virtuel. S'il existe des objets créés dynamiquement appartenant à la sous-classe, ils ne seront pas récupérés par le destructeur de base lors d'un appel à supprimer. Vous allez bien, alors BLUURRK! (difficile de trouver où aussi.)
Chris Reid
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.