Pourquoi devrais-je déclarer un destructeur virtuel pour une classe abstraite en C ++?

165

Je sais que c'est une bonne pratique de déclarer des destructeurs virtuels pour les classes de base en C ++, mais est-il toujours important de déclarer des virtualdestructeurs même pour les classes abstraites qui fonctionnent comme des interfaces? Veuillez fournir quelques raisons et exemples.

Kevin
la source

Réponses:

196

C'est encore plus important pour une interface. Tout utilisateur de votre classe tiendra probablement un pointeur vers l'interface, pas un pointeur vers l'implémentation concrète. Quand ils viendront à le supprimer, si le destructeur n'est pas virtuel, ils appelleront le destructeur de l'interface (ou la valeur par défaut fournie par le compilateur, si vous n'en avez pas spécifié un), pas le destructeur de la classe dérivée. Fuite de mémoire instantanée.

Par exemple

class Interface
{
   virtual void doSomething() = 0;
};

class Derived : public Interface
{
   Derived();
   ~Derived() 
   {
      // Do some important cleanup...
   }
};

void myFunc(void)
{
   Interface* p = new Derived();
   // The behaviour of the next line is undefined. It probably 
   // calls Interface::~Interface, not Derived::~Derived
   delete p; 
}
Airsource Ltd
la source
4
delete pinvoque un comportement non défini. Il n'est pas garanti d'appeler Interface::~Interface.
Mankarse
@Mankarse: pouvez-vous expliquer ce qui le rend indéfini? Si Derived n'implémentait pas son propre destructeur, serait-il toujours un comportement indéfini?
Ponkadoodle
14
@Wallacoloo est indéfini en raison de [expr.delete]/: ... if the static type of the object to be deleted is different from its dynamic type, ... the static type shall have a virtual destructor or the behavior is undefined. .... Il ne serait toujours pas défini si Derived utilisait un destructeur généré implicitement.
Mankarse
37

La réponse à votre question est souvent, mais pas toujours. Si votre classe abstraite interdit aux clients d'appeler delete sur un pointeur vers elle (ou si elle le dit dans sa documentation), vous êtes libre de ne pas déclarer de destructeur virtuel.

Vous pouvez interdire aux clients d'appeler delete sur un pointeur vers lui en protégeant son destructeur. En travaillant comme ça, il est parfaitement sûr et raisonnable d'omettre un destructeur virtuel.

Vous finirez par vous retrouver sans table de méthode virtuelle, et vous finirez par signaler à vos clients votre intention de la rendre non supprimable via un pointeur vers elle, vous avez donc en effet des raisons de ne pas la déclarer virtuelle dans ces cas.

[Voir le point 4 de cet article: http://www.gotw.ca/publications/mill18.htm ]

Johannes Schaub - litb
la source
La clé pour que votre réponse fonctionne est "sur lequel la suppression n'est pas appelée". Habituellement, si vous avez une classe de base abstraite conçue pour être une interface, delete sera appelée sur la classe d'interface.
John Dibling
Comme John l'a souligné ci-dessus, ce que vous suggérez est assez dangereux. Vous vous fiez à l'hypothèse que les clients de votre interface ne détruiront jamais un objet ne connaissant que le type de base. La seule façon de garantir que si ce n'est pas virtuel est de protéger le dtor de la classe abstraite.
Michel
Michel, je l'ai dit :) "Si vous faites cela, vous protégez votre destructeur. Si vous le faites, les clients ne pourront pas supprimer en utilisant un pointeur vers cette interface." et en effet, il ne dépend pas des clients, mais il doit le faire respecter en disant aux clients "vous ne pouvez pas faire ...". Je ne vois aucun danger
Johannes Schaub - litb
J'ai corrigé la mauvaise formulation de ma réponse maintenant. il le déclare explicitement maintenant qu'il ne dépend pas des clients. en fait, je pensais qu'il était évident que compter sur les clients pour faire quelque chose est de toute façon un problème. merci :)
Johannes Schaub - litb
2
+1 pour mentionner les destructeurs protégés, qui sont l'autre «moyen de sortir» du problème d'appeler accidentellement le mauvais destructeur lors de la suppression d'un pointeur vers une classe de base.
j_random_hacker
23

J'ai décidé de faire quelques recherches et d'essayer de résumer vos réponses. Les questions suivantes vous aideront à décider du type de destructeur dont vous avez besoin:

  1. Votre classe est-elle destinée à être utilisée comme classe de base?
    • Non: Déclarez un destructeur non virtuel public pour éviter le v-pointer sur chaque objet de la classe * .
    • Oui: lisez la question suivante.
  2. Votre classe de base est-elle abstraite? (c'est-à-dire des méthodes pures virtuelles?)
    • Non: essayez de rendre votre classe de base abstraite en remodelant votre hiérarchie de classes
    • Oui: lisez la question suivante.
  3. Voulez-vous autoriser la suppression polymorphe via un pointeur de base?
    • Non: Déclarez le destructeur virtuel protégé pour empêcher l'utilisation indésirable.
    • Oui: Déclarez le destructeur virtuel public (pas de surcharge dans ce cas).

J'espère que ça aide.

* Il est important de noter qu'il n'y a aucun moyen en C ++ de marquer une classe comme finale (c'est-à-dire non sous-classable), donc dans le cas où vous décidez de déclarer votre destructeur non virtuel et public, n'oubliez pas d'avertir explicitement vos collègues programmeurs contre dérivant de votre classe.

Références:

Davidag
la source
11
Cette réponse est en partie obsolète, il existe désormais un mot clé final en C ++.
Étienne
10

Oui, c'est toujours important. Les classes dérivées peuvent allouer de la mémoire ou contenir des références à d'autres ressources qui devront être nettoyées lorsque l'objet est détruit. Si vous ne donnez pas à vos interfaces / classes abstraites de destructeurs virtuels, alors chaque fois que vous supprimez une instance de classe dérivée via un handle de classe de base, votre destructeur de classe dérivée ne sera pas appelé.

Par conséquent, vous ouvrez le potentiel de fuites de mémoire

class IFoo
{
  public:
    virtual void DoFoo() = 0;
};

class Bar : public IFoo
{
  char* dooby = NULL;
  public:
    virtual void DoFoo() { dooby = new char[10]; }
    void ~Bar() { delete [] dooby; }
};

IFoo* baz = new Bar();
baz->DoFoo();
delete baz; // memory leak - dooby isn't deleted
JO.
la source
Certes, en fait, dans cet exemple, il se peut que ce ne soit pas seulement une fuite de mémoire, mais éventuellement un crash: - /
Evan Teran
7

Ce n'est pas toujours obligatoire, mais je trouve que c'est une bonne pratique. Ce qu'il fait, c'est qu'il permet à un objet dérivé d'être supprimé en toute sécurité via un pointeur d'un type de base.

Donc par exemple:

Base *p = new Derived;
// use p as you see fit
delete p;

est mal formé s'il Basen'a pas de destructeur virtuel, car il tentera de supprimer l'objet comme s'il s'agissait d'un fichier Base *.

Evan Teran
la source
ne voulez-vous pas corriger boost :: shared_pointer p (new Derived) pour qu'il ressemble à boost :: shared_pointer <Base> p (new Derived); ? peut-être que ppl comprendra votre réponse et votera
Johannes Schaub - litb
EDIT: "Codifié" quelques parties pour rendre les chevrons visibles, comme le suggère le litb.
j_random_hacker
@EvanTeran: Je ne sais pas si cela a changé depuis la publication de la réponse (la documentation Boost sur boost.org/doc/libs/1_52_0/libs/smart_ptr/shared_ptr.htm suggère que cela a peut-être été le cas), mais ce n'est pas vrai ces jours- shared_ptrci, qui tentera de supprimer l'objet comme s'il s'agissait d'un Base *- il se souvient du type de chose avec lequel vous l'avez créé. Voir le lien référencé, en particulier le bit qui dit "Le destructeur appellera delete avec le même pointeur, complet avec son type d'origine, même lorsque T n'a pas de destructeur virtuel, ou est nul."
Stuart Golodetz
@StuartGolodetz: Hmm, vous avez peut-être raison, mais honnêtement, je ne suis pas sûr. Il peut encore être mal formé dans ce contexte en raison du manque de destructeur virtuel. Cela vaut la peine d'être examiné.
Evan Teran
@EvanTeran: Au cas où cela serait utile - stackoverflow.com/questions/3899790/shared-ptr-magic .
Stuart Golodetz
5

Ce n'est pas seulement une bonne pratique. C'est la règle n ° 1 pour toute hiérarchie de classes.

  1. La classe la plus de base d'une hiérarchie en C ++ doit avoir un destructeur virtuel

Maintenant pour le pourquoi. Prenez la hiérarchie animale typique. Les destructeurs virtuels passent par la répartition virtuelle comme tout autre appel de méthode. Prenons l'exemple suivant.

Animal* pAnimal = GetAnimal();
delete pAnimal;

Supposons que Animal est une classe abstraite. Le seul moyen pour C ++ de connaître le destructeur approprié à appeler est via la répartition de méthode virtuelle. Si le destructeur n'est pas virtuel, il appellera simplement le destructeur d'Animal et ne détruira aucun objet dans les classes dérivées.

La raison pour laquelle le destructeur est virtuel dans la classe de base est qu'il supprime simplement le choix des classes dérivées. Leur destructeur devient virtuel par défaut.

JaredPar
la source
2
Je suis généralement d' accord avec vous, car généralement lors de la définition d'une hiérarchie, vous voulez pouvoir faire référence à un objet dérivé à l'aide d'un pointeur / référence de classe de base. Mais ce n'est pas toujours le cas, et dans ces autres cas, il peut suffire de protéger la classe de base dtor à la place.
j_random_hacker
@j_random_hacker le rendant protégé ne vous protégera pas des suppressions internes incorrectes
JaredPar
1
@JaredPar: C'est vrai, mais au moins vous pouvez être responsable dans votre propre code - le plus difficile est de vous assurer que le code client ne peut pas provoquer l'explosion de votre code. (De même, rendre un membre de données privé n'empêche pas le code interne de faire quelque chose de stupide avec ce membre.)
j_random_hacker
@j_random_hacker, désolé de répondre avec un article de blog mais cela correspond vraiment à ce scénario. blogs.msdn.com/jaredpar/archive/2008/03/24/…
JaredPar
@JaredPar: Excellent post, je suis d'accord avec vous à 100%, en particulier sur la vérification des contrats dans le code de vente au détail. Je veux juste dire qu'il y a des cas où vous savez que vous n'avez pas besoin d'un décor virtuel. Exemple: classes de balises pour l'envoi de modèles. Ils ont une taille de 0, vous n'utilisez l'héritage que pour indiquer les spécialisations.
j_random_hacker
3

La réponse est simple, vous avez besoin qu'elle soit virtuelle sinon la classe de base ne serait pas une classe polymorphe complète.

    Base *ptr = new Derived();
    delete ptr; // Here the call order of destructors: first Derived then Base.

Vous préférez la suppression ci-dessus, mais si le destructeur de la classe de base n'est pas virtuel, seul le destructeur de la classe de base sera appelé et toutes les données de la classe dérivée resteront non supprimées.

fatma.ekici
la source