Quand ne devriez-vous pas utiliser de destructeurs virtuels?

99

Existe-t-il une bonne raison de ne pas déclarer de destructeur virtuel pour une classe? Quand devriez-vous spécifiquement éviter d'en écrire un?

Roader Mag
la source

Réponses:

72

Il n'est pas nécessaire d'utiliser un destructeur virtuel lorsque l'une des conditions ci-dessous est vraie:

  • Aucune intention d'en tirer des classes
  • Aucune instanciation sur le tas
  • Aucune intention de stocker dans un pointeur d'une superclasse

Aucune raison particulière de l'éviter à moins que vous ne soyez vraiment pressé par la mémoire.

SEP
la source
25
Ce n'est pas une bonne réponse. "Il n'y a pas besoin" est différent de "ne devrait pas", et "aucune intention" est différent de "rendu impossible".
Programmeur Windows
5
Ajouter également: aucune intention de supprimer une instance via un pointeur de classe de base.
Adam Rosenfield
9
Cela ne répond pas vraiment à la question. Quelle est votre bonne raison de ne pas utiliser de décor virtuel?
mxcl
9
Je pense que lorsqu'il n'est pas nécessaire de faire quelque chose, c'est une bonne raison pour ne pas le faire. Il suit le principe de conception simple de XP.
sep
12
En disant que vous n'avez «aucune intention», vous faites une énorme hypothèse sur la façon dont votre classe sera utilisée. Il me semble que la solution la plus simple dans la plupart des cas (ce qui devrait donc être la valeur par défaut) devrait être d'avoir des destructeurs virtuels, et de les éviter uniquement si vous avez une raison spécifique de ne pas le faire. Je suis donc toujours curieux de savoir quelle serait une bonne raison.
ckarras
68

Pour répondre explicitement à la question, c'est-à-dire quand ne pas déclarer un destructeur virtuel.

C ++ '98 / '03

L'ajout d'un destructeur virtuel peut changer votre classe de POD (données anciennes simples) * ou l'agréger en non-POD. Cela peut empêcher la compilation de votre projet si votre type de classe est initialisé quelque part.

struct A {
  // virtual ~A ();
  int i;
  int j;
};
void foo () { 
  A a = { 0, 1 };  // Will fail if virtual dtor declared
}

Dans un cas extrême, un tel changement peut également provoquer un comportement indéfini où la classe est utilisée d'une manière qui nécessite un POD, par exemple en la passant via un paramètre de points de suspension, ou en l'utilisant avec memcpy.

void bar (...);
void foo (A & a) { 
  bar (a);  // Undefined behavior if virtual dtor declared
}

[* Un type POD est un type qui a des garanties spécifiques sur sa disposition de mémoire. La norme dit en réalité que si vous copiez à partir d'un objet de type POD dans un tableau de caractères (ou non signés) et inversement, le résultat sera le même que celui de l'objet d'origine.]

C ++ moderne

Dans les versions récentes de C ++, le concept de POD était divisé entre la disposition des classes et sa construction, sa copie et sa destruction.

Pour le cas des points de suspension, ce n'est plus un comportement indéfini, il est maintenant pris en charge conditionnellement avec une sémantique définie par l'implémentation (N3937 - ~ C ++ '14 - 5.2.2 / 7):

... Passer un argument potentiellement évalué de type classe (Article 9) ayant un constructeur de copie non trivial, un constructeur de déplacement non trivial, ou un destructeur on-trivial, sans paramètre correspondant, est conditionnellement pris en charge avec l'implémentation- sémantique définie.

Déclarer un destructeur autre que =defaultsignifie que ce n'est pas trivial (12.4 / 5)

... Un destructeur est trivial s'il n'est pas fourni par l'utilisateur ...

D'autres modifications apportées au C ++ moderne réduisent l'impact du problème d'initialisation de l'agrégat car un constructeur peut être ajouté:

struct A {
  A(int i, int j);
  virtual ~A ();
  int i;

  int j;
};
void foo () { 
  A a = { 0, 1 };  // OK
}
Richard Corden
la source
1
Vous avez raison et j'avais tort, la performance n'est pas la seule raison. Mais cela montre que j'avais raison sur le reste: le programmeur de la classe ferait mieux d'inclure du code pour éviter que la classe ne soit héritée par quelqu'un d'autre.
Programmeur Windows
cher Richard, pouvez-vous s'il vous plaît commenter un peu plus ce que vous avez écrit. Je ne comprends pas votre point, mais cela semble le seul point précieux que j'ai trouvé en googlant) Ou peut-être pouvez-vous donner un lien vers une explication plus détaillée?
John Smith
1
@JohnSmith J'ai mis à jour la réponse. Espérons que cela aide.
Richard Corden
28

Je déclare un destructeur virtuel si et seulement si j'ai des méthodes virtuelles. Une fois que j'ai des méthodes virtuelles, je ne me fais pas confiance pour éviter de l'instancier sur le tas ou de stocker un pointeur vers la classe de base. Ces deux opérations sont extrêmement courantes et entraîneront souvent des fuites de ressources silencieuses si le destructeur n'est pas déclaré virtuel.

Andy
la source
3
Et, en fait, il y a une option d'avertissement sur gcc qui avertit précisément sur ce cas (méthodes virtuelles mais pas de dtor virtuel).
CesarB
6
Ne courez-vous pas alors le risque de fuir de mémoire si vous dérivez de la classe, que vous ayez ou non d'autres fonctions virtuelles?
Mag Roader
1
Je suis d'accord avec mag. Cette utilisation d'un destructeur virtuel et / ou d'une méthode virtuelle sont des exigences distinctes. Le destructeur virtuel permet à une classe d'effectuer un nettoyage (par exemple, supprimer de la mémoire, fermer des fichiers, etc.) ET assure également que les constructeurs de tous ses membres sont appelés.
user48956
7

Un destructeur virtuel est nécessaire chaque fois qu'il y a une chance qui deletepourrait être appelé sur un pointeur vers un objet d'une sous-classe avec le type de votre classe. Cela garantit que le destructeur correct est appelé au moment de l'exécution sans que le compilateur ait à connaître la classe d'un objet sur le tas au moment de la compilation. Par exemple, supposons que Bc'est une sous-classe de A:

A *x = new B;
delete x;     // ~B() called, even though x has type A*

Si votre code n'est pas critique pour les performances, il serait raisonnable d'ajouter un destructeur virtuel à chaque classe de base que vous écrivez, juste pour la sécurité.

Cependant, si vous vous retrouvez avec deletebeaucoup d'objets dans une boucle serrée, la surcharge de performances liée à l'appel d'une fonction virtuelle (même vide) peut être perceptible. Le compilateur ne peut généralement pas intégrer ces appels et le processeur peut avoir du mal à prédire où aller. Il est peu probable que cela ait un impact significatif sur les performances, mais cela vaut la peine d'être mentionné.

Jay Conrod
la source
"Si votre code n'est pas critique pour les performances, il serait raisonnable d'ajouter un destructeur virtuel à chaque classe de base que vous écrivez, juste pour la sécurité." devrait être souligné plus dans chaque réponse que je vois
csguy
5

Les fonctions virtuelles signifient que chaque objet alloué augmente le coût de la mémoire par un pointeur de table de fonction virtuelle.

Donc, si votre programme implique d'allouer un très grand nombre d'objets, il vaudrait la peine d'éviter toutes les fonctions virtuelles afin d'économiser les 32 bits supplémentaires par objet.

Dans tous les autres cas, vous vous épargnerez la misère de débogage pour rendre le dtor virtuel.

mxcl
la source
1
Juste une petite piqûre, mais ces jours-ci, un pointeur sera souvent 64 bits au lieu de 32.
Head Geek
5

Toutes les classes C ++ ne conviennent pas pour une utilisation en tant que classe de base avec un polymorphisme dynamique.

Si vous voulez que votre classe soit adaptée au polymorphisme dynamique, alors son destructeur doit être virtuel. De plus, toutes les méthodes qu'une sous-classe pourrait vouloir remplacer (ce qui pourrait signifier toutes les méthodes publiques, plus éventuellement certaines protégées utilisées en interne) doivent être virtuelles.

Si votre classe n'est pas adaptée au polymorphisme dynamique, alors le destructeur ne doit pas être marqué virtuel, car cela est trompeur. Cela encourage simplement les gens à utiliser votre classe de manière incorrecte.

Voici un exemple de classe qui ne conviendrait pas au polymorphisme dynamique, même si son destructeur était virtuel:

class MutexLock {
    mutex *mtx_;
public:
    explicit MutexLock(mutex *mtx) : mtx_(mtx) { mtx_->lock(); }
    ~MutexLock() { mtx_->unlock(); }
private:
    MutexLock(const MutexLock &rhs);
    MutexLock &operator=(const MutexLock &rhs);
};

Le but de cette classe est de s'asseoir sur la pile pour RAII. Si vous passez des pointeurs vers des objets de cette classe, sans parler de ses sous-classes, alors vous faites le mal.

Steve Jessop
la source
2
L'utilisation polymorphe n'implique pas de suppression polymorphe. Il existe de nombreux cas d'utilisation pour une classe d'avoir des méthodes virtuelles mais pas de destructeur virtuel. Considérez une boîte de dialogue définie statiquement typique, dans à peu près n'importe quelle boîte à outils GUI. La fenêtre parente détruira les objets enfants, et elle connaît le type exact de chacun, mais toutes les fenêtres enfants seront également utilisées de manière polymorphe à n'importe quel nombre d'endroits, tels que les tests de hit, le dessin, les API d'accessibilité qui récupèrent le texte pour le texte- moteurs vocaux, etc.
Ben Voigt
4
C'est vrai, mais le questionneur demande quand vous devez spécifiquement éviter un destructeur virtuel. Pour la boîte de dialogue que vous décrivez, un destructeur virtuel est inutile, mais IMO pas dangereux. Je ne suis pas sûr d'être sûr de ne jamais avoir besoin de supprimer une boîte de dialogue à l'aide d'un pointeur de classe de base - par exemple, je peux à l'avenir vouloir que ma fenêtre parente crée ses objets enfants à l'aide d'usines. Il ne s'agit donc pas d' éviter un destructeur virtuel, mais simplement de ne pas prendre la peine d'en avoir un. Un destructeur virtuel sur une classe qui ne convient pas à la dérivation est cependant dangereux, car il est trompeur.
Steve Jessop
4

Une bonne raison pour ne pas déclarer un destructeur comme virtuel est lorsque cela évite à votre classe d'avoir une table de fonctions virtuelle ajoutée, et vous devriez éviter cela autant que possible.

Je sais que beaucoup de gens préfèrent toujours déclarer les destructeurs comme virtuels, juste pour être du bon côté. Mais si votre classe n'a pas d'autres fonctions virtuelles, alors il n'y a vraiment aucun intérêt à avoir un destructeur virtuel. Même si vous donnez votre classe à d'autres personnes qui en dérivent ensuite d'autres classes, elles n'auraient aucune raison d'appeler delete sur un pointeur qui a été transmis à votre classe - et si elles le font, je considérerais cela comme un bogue.

D'accord, il y a une seule exception, à savoir si votre classe est (mal-) utilisée pour effectuer une suppression polymorphe d'objets dérivés, mais alors vous - ou les autres gars - sachez que cela nécessite un destructeur virtuel.

En d'autres termes, si votre classe a un destructeur non virtuel, c'est une déclaration très claire: "Ne m'utilisez pas pour supprimer des objets dérivés!"

kidfisto
la source
3

Si vous avez une très petite classe avec un grand nombre d'instances, la surcharge d'un pointeur vtable peut faire une différence dans l'utilisation de la mémoire de votre programme. Tant que votre classe n'a pas d'autres méthodes virtuelles, rendre le destructeur non virtuel économisera cette surcharge.

Mark Ransom
la source
1

Je déclare généralement le destructeur virtuel, mais si vous avez un code critique pour les performances utilisé dans une boucle interne, vous voudrez peut-être éviter la recherche de table virtuelle. Cela peut être important dans certains cas, comme la vérification des collisions. Mais faites attention à la façon dont vous détruisez ces objets si vous utilisez l'héritage, sinon vous ne détruirez que la moitié de l'objet.

Notez que la recherche de table virtuelle se produit pour un objet si une méthode sur cet objet est virtuelle. Donc, inutile de supprimer la spécification virtuelle sur un destructeur si vous avez d'autres méthodes virtuelles dans la classe.

Jørn Jensen
la source
1

Si vous devez absolument vous assurer que votre classe n'a pas de vtable, vous ne devez pas non plus avoir de destructeur virtuel.

C'est un cas rare, mais cela arrive.

Les classes DirectX D3DVECTOR et D3DMATRIX sont l'exemple le plus courant d'un modèle qui fait cela. Ce sont des méthodes de classe au lieu de fonctions pour le sucre syntaxique, mais les classes n'ont intentionnellement pas de vtable afin d'éviter la surcharge de fonction parce que ces classes sont spécifiquement utilisées dans la boucle interne de nombreuses applications hautes performances.

Lisa
la source
0

Sur l'opération qui sera effectuée sur la classe de base, et qui devrait se comporter virtuellement, devrait être virtuelle. Si la suppression peut être effectuée de manière polymorphe via l'interface de classe de base, elle doit alors se comporter virtuellement et être virtuelle.

Le destructeur n'a pas besoin d'être virtuel si vous n'avez pas l'intention de dériver de la classe. Et même si vous le faites, un destructeur non virtuel protégé est tout aussi bon si la suppression des pointeurs de classe de base n'est pas nécessaire .

la glace
la source
-7

La réponse de la performance est la seule que je connaisse qui ait une chance d'être vraie. Si vous avez mesuré et constaté que la dé-virtualisation de vos destructeurs accélère vraiment les choses, alors vous avez probablement d'autres choses dans cette classe qui doivent également être accélérées, mais à ce stade, il y a des considérations plus importantes. Un jour, quelqu'un va découvrir que votre code leur fournirait une classe de base intéressante et leur épargnerait une semaine de travail. Vous feriez mieux de vous assurer qu'ils font le travail de cette semaine, en copiant et en collant votre code, au lieu d'utiliser votre code comme base. Vous feriez mieux de vous assurer de rendre certaines de vos méthodes importantes privées afin que personne ne puisse jamais hériter de vous.

Programmeur Windows
la source
Le polymorphisme ralentira certainement les choses. Comparez-le avec une situation où nous avons besoin de polymorphisme et choisissez de ne pas le faire, ce sera encore plus lent. Exemple: nous implémentons toute la logique au niveau du destructeur de classe de base, en utilisant RTTI et une instruction switch pour nettoyer les ressources.
sep
1
En C ++, il n'est pas de votre responsabilité de m'empêcher d'hériter de vos classes que vous avez documentées ne sont pas adaptées à une utilisation en tant que classes de base. Il est de ma responsabilité d'utiliser l'héritage avec prudence. À moins que le guide de style de la maison ne dise le contraire, bien sûr.
Steve Jessop
1
... simplement rendre le destructeur virtuel ne signifie pas que la classe fonctionnera nécessairement correctement en tant que classe de base. Donc, le marquer virtuel «juste parce que», au lieu de faire cette évaluation, c'est écrire un chèque que mon code ne peut pas encaisser.
Steve Jessop