web-dev-qa-db-fra.com

Quand ne devriez-vous pas utiliser des destructeurs virtuels?

Y a-t-il jamais une bonne raison pour pas déclarer un destructeur virtuel pour une classe? Quand devez-vous spécifiquement éviter d'en écrire un?

92
Mag Roader

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
  • Pas d'instanciation sur le tas
  • Aucune intention de stocker dans un pointeur d'une superclasse

Aucune raison spécifique de l'éviter, sauf si vous êtes vraiment pressé par la mémoire.

68
sep

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

C++ '98/'03

L'ajout d'un destructeur virtuel peut faire passer votre classe de POD (données anciennes simples) * ou d'agrégat à non-POD. Cela peut empêcher la compilation de votre projet si votre type de classe est agrégé 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 lorsque la classe est utilisée d'une manière qui nécessite un POD, par exemple en le passant via un paramètre Ellipsis, 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 quant à sa disposition en mémoire. La norme dit seulement que si vous copiez à partir d'un objet de type POD dans un tableau de caractères (ou de caractères non signés) et vice-versa, le résultat sera le même que 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 Ellipsis, ce n'est plus un comportement indéfini, il est désormais pris en charge sous condition 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 signifiera que ce n'est pas anodin (12.4/5)

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

D'autres modifications apportées à Modern C++ réduisent l'impact du problème d'initialisation agrégée 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
}
67
Richard Corden

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 les instancier sur le tas ou de stocker un pointeur sur la classe de base. Ces deux opérations sont extrêmement courantes et entraînent souvent une fuite silencieuse des ressources si le destructeur n'est pas déclaré virtuel.

25
Andy

Un destructeur virtuel est nécessaire chaque fois qu'il existe un risque que delete soit 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 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 en termes de performances, il serait raisonnable d'ajouter un destructeur virtuel à chaque classe de base que vous écrivez, juste pour des raisons de sécurité.

Cependant, si vous vous êtes retrouvé delete à utiliser de nombreux objets dans une boucle étroite, la surcharge de performances de l'appel d'une fonction virtuelle (même vide) peut être perceptible. Le compilateur ne peut généralement pas aligner 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 il convient de le mentionner.

6
Jay Conrod

Toutes les classes C++ ne conviennent pas à une utilisation comme classe de base avec 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 potentiellement certaines protégées utilisées en interne) doivent être virtuelles.

Si votre classe ne convient pas au polymorphisme dynamique, le destructeur ne doit pas être marqué comme 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);
};

L'intérêt de cette classe est de s'asseoir sur la pile pour RAII. Si vous passez des pointeurs vers des objets de cette classe, et encore moins des sous-classes, vous vous trompez.

5
Steve Jessop

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, cela vaut 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.

5
mxcl

Une bonne raison pour ne pas déclarer un destructeur comme virtuel est lorsque cela évite à votre classe d'avoir une table de fonction 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 sûr. Mais si votre classe n'a pas d'autres fonctions virtuelles, il n'y a vraiment, 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é transféré à votre classe - et si c'est le cas, je considérerais cela comme un bug.

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

Autrement dit, si votre classe a un destructeur non virtuel, alors ceci est une déclaration très claire: "Ne m'utilisez pas pour supprimer des objets dérivés!"

4
kidfisto

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 permettra d'économiser cette surcharge.

3
Mark Ransom

Si vous devez absolument vous assurer que votre classe n'a pas de table virtuelle, vous ne devez pas avoir de destructeur virtuel également.

C'est un cas rare, mais cela arrive.

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

1
Lisa

Je déclare généralement le destructeur virtuel, mais si vous avez du code critique de performance 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 la méthode any sur cet objet est virtuelle. Il est donc inutile de supprimer la spécification virtuelle sur un destructeur si vous avez d'autres méthodes virtuelles dans la classe.

1
Jørn Jensen

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, n destructeur non virtuel protégé est tout aussi efficace si la suppression des pointeurs de classe de base n'est pas requise.

0
icecrime