Quand devrait-on marquer un destructeur comme étant virtuel?

Plusieurs ont longtemps véhiculé l'idée qu'un destructeur devrait toujours être virtuel. En réalité, qu'en est-il vraiment? Est-ce un mythe ou une consigne à respecter?

Les avantages d'un destructeur virtuel

En résumé, un destructeur virtuel fait en sorte que si un programme détruit une abstraction polymorphique allouée dynamiquement, le bon destructeur sera invoqué.

class X {
   // diverses méthodes
public:
   virtual ~X(); // défini quelque part
};
class Y : public X {
   // diverses méthodes
public:
   ~Y(); // défini quelque part
};
class Z : public X {
   // diverses méthodes
public:
   ~Z(); // défini quelque part
};
int main() {
   X *pX = new Y; // légal: un Y est un X
   // ...
   // ceci invoquera Y::~Y() même si pX est un X* parce que le destructeur
   // de X est virtuel. S'il ne l'était pas, alors X::~X() serait appelé
   delete pX;
}

Dans l'exemple ci-dessus, la classe X est une abstraction dont dérivent les classes Y et Z. Pour donner des noms plus concrets, X pourrait être la classe Forme alors que Y et Z pourraient être Cercle et Hexagone.

Le programme principal crée dynamiquement un Y et affecte le pointeur résultant à un pointeur de X, ce qui est légal car Y dérive publiquement de X – un Y est un X au vu et au su de tous).

À travers le pointeur pX, main() peut invoquer toutes les méthodes exposées publiquement par la classe X, car ce sont des méthodes que l'objet au bout du pointeur possède nécessairement (un Y est au moins un X, donc un Y possède au moins ce que possède un X).

La présence d'un destructeur virtuel dans X ici fait en sorte que l'opération delete pX; invoque le destructeur de l'objet réellement pointé par pX, peu importe son type.

Si le destructeur de X n'était pas virtuel, alors delete pX; invoquerait X::~X() tout simplement. Si Y::~Y() doit libérer des ressources ou faire un peu de nettoyage, invoquer le mauvais constructeur est fortement dommageable.

Notez que la situation est la même si vous utilisez un pointeur intelligent :

#include <memory>
class X {
   // diverses méthodes
public:
   virtual ~X();
};
class Y : public X {
   // diverses méthodes
public:
   ~Y();
};
class Z : public X {
   // diverses méthodes
public:
   ~Z();
};
int main() {
   using namespace std;
   unique_ptr<X> pX { new Y }; // légal: un Y est un X
   // ...
   // la fin de portée de pX invoquera Y::~Y() même si pX est un unique_ptr<X> parce que le destructeur
   // de X est virtuel. S'il ne l'était pas, alors X::~X() serait appelé
}

Il est important d'appeler le bon destructeur sur un objet; le compilateur voit le type dit « statique » (dans nos exemples ci-dessus, X* et unique_ptr<X> respectivement), or si le type statique diffère du type « dynamique » (Y* dans les deux cas), l'appel d'un destructeur inapproprié entraîne du comportement indéfini.

Le coût d'un destructeur virtuel

Insérer au moins une méthode virtuelle dans un objet en accroît la taille (il faut injecter une table de pointeurs et un peu d'information sémantique supplémentaire dans la description de la classe). Dans les systèmes où les ressources sont rares, comme par exemple dans le cas des systèmes embarqués, ce coût peut peser dans la balance. Par exemple, le programme suivant :

#include <iostream>
struct X {
   int f() const { return 3; }
   virtual ~X() = default;
} x;
struct Y {
   int f() const { return 3; }
   ~Y() = default;
} y;
int main() {
   using namespace std;
   cout << "sizeof x : " << sizeof x << '\n'
        << "sizeof y : " << sizeof y << endl;
}

... affichera probablement quelque chose comme (voir https://wandbox.org/permlink/tQzwyyhwn3e2zZMK) :

sizeof x : 8
sizeof y : 1

Aussi, invoquer une méthode virtuelle est plus coûteux qu'invoquer une méthode qui ne l'est pas, du fait qu'il faut passer par une indireciton (par un pointeur) pour réaliser l'invocation. Prises isolément, ces invocations ne coûtent pas grand chose, mais si elles apparaissent dans des secteurs critiques d'un programme elles peuvent entraîner une dégradation de la performance.

Ce qu'il faut comprendre ici est que les méthods virtuelles (donc le polymorphisme) constituent un bon outil, mais comme pour tous les outils il faut choisir avec soin le moment où on y a recours et se demander s'il s'agit du bon outil avant d'y avoir recours. Il y a un coût: si nous choisissons de le payer, c'est que nous en avons fait le choix consciemment.

Alors les destructeurs devraient-ils tous être virtuels?

En bref : non. Comme le veut l'adage selon lequel en présence d'un marteau doré, on tend à voir des clous partout, il n'est pas sage d'appliquer une technique systématiquement s'il y a un prix à payer pour y avoir recours et s'il n'est pas certain qu'on en ait besoin. Ce serait du gaspillage. Certains destructeurs doivent être virtuels pour que les programmes soient solides, d'autres ne devraient jamais l'être.

La suggestion traditionnelle de toujours déclarer les destructeurs virtuels remonte à l'époque où notre pensée objet se formait et se construisait. Nous comprenons mieux aujourd'hui les tenants et aboutissants de nos gestes et nous sommes moins dépendants de maximes pour faciliter notre processus de décision.

Dans quelles circonstances un destructeur devrait-il être virtuel?

Le truc est simple : si nous voulons un destructeur virtuel dans une classe X, c'est nécessairement parce que la classe X servira d'abstraction pour d'autres classes. Cela signifie qu'un programme voudra obtenir un pointeur ou une référence sur un X sans vraiment savoir s'il y a un X ou quelque chose de plus spécialisé derrière. Dans tous les cas où un destructeur virtuel serait pertinent, X aura un comportement polymorphique.

Ainsi, dans une classe donnée, on ne spécifie un destructeur virtuel que si cette classe a au moins une autre méthode virtuelle. Si une classe n'expose aucune méthode virtuelle, alors elle ne peut servir d'abstraction polymorphique et un destructeur virtuel ne servirait qu'à grossir et à ralentir la classe.

Notez que le polymorphisme est une caractéristique héritée. Ainsi, si un parent a un destructeur virtuel, alors celui de tous ses enfants l'est aussi. Il n'est donc utile de se poser la question virtuel ou non? que localement. Si une classe expose au moins une méthode virtuelle, alors il est pertinent d'offrir un destructeur virtuel, point à la ligne.

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !