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?
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?
Réponses:
Il n'est pas nécessaire d'utiliser un destructeur virtuel lorsque l'une des conditions ci-dessous est vraie:
Aucune raison particulière de l'éviter à moins que vous ne soyez vraiment pressé par la mémoire.
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 =default
signifie 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
}
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.
Un destructeur virtuel est nécessaire chaque fois qu'il y a une chance qui delete
pourrait ê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 B
c'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 delete
beaucoup 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é.
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.
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.
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!"
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.
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.
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.
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 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.