I. Problèmes▲
I-A. Question Junior▲
Qu'est-ce qu'une fonction virtuelle pure ? Donnez un exemple.
I-B. Question Guru▲
Pourquoi déclarer une fonction virtuelle pure et aussi écrire une définition (body) ? Donnez autant de raisons ou de situations que vous pouvez.
II. Solutions▲
II-A. Question Junior▲
Qu'est-ce qu'une fonction virtuelle pure ? Donnez un exemple.
Une fonction virtuelle pure est une fonction virtuelle dont vous voulez forcer l'implémentation par les classes dérivées. Si une classe comporte encore des fonctions virtuelles non redéfinies, alors c'est une classe abstraite, et vous ne pourrez pas instancier ce type.
class
AbstractClass {
public
:
// déclare une fonction virtuelle pure :
// cette classe est désormais abstraite
virtual
void
f(int
) =
0
;
}
;
class
StillAbstract : public
AbstractClass {
// impossible d'instancier f(int),
// aussi cette classe est-elle toujours abstraite
}
;
class
Concrete : public
StillAbstract {
public
:
// instancie f(int),
// aussi cette classe est concrète
void
f(int
) {
/*...*/
}
}
;
AbstractClass a; // erreur, classe abstraite
StillAbstract b; // erreur, classe abstraite
Concrete c; // ok, classe concrète
II-B. Question Guru▲
Pourquoi déclarer une fonction virtuelle pure et aussi écrire une définition (body) ? Donnez autant de raisons ou de situations que vous pouvez.
Il y a trois principales raisons pour faire cela : la première est courante, la deuxième est assez rare et la troisième est une solution de rechange utilisée de temps en temps par des programmeurs expérimentés travaillant avec des compilateurs un peu faibles. La plupart des programmeurs ne devraient jamais utiliser que la première.
II-B-1. Destructeur virtuel pur▲
Toutes les classes de base devraient avoir un destructeur virtuel (consultez votre manuel C++ favori pour y trouver les raisons).(1) Si la classe doit être abstraite (vous voulez éviter son instanciation) mais qu'il s'avère qu'elle n'a aucune autre fonction virtuelle pure, voici une technique courante pour créer le destructeur virtuel pur :
// file b.h
class
B {
public
:
/*...d'autres choses...*/
virtual
~
B() =
0
; // destructeur virtuel pur
}
;
Bien sûr, tout destructeur de classe dérivée doit appeler le destructeur de classe de base, ainsi le destructeur doit rester défini (même s'il est vide) :
// file b.cpp
B::
~
B() {
/* peut être vide */
}
Si cette définition ne devait pas être fournie, vous pourriez toujours dériver les classes de B, mais elles ne pourraient jamais être instanciées, ce qui n'est pas particulièrement utile.
II-B-2. Acceptation consciente forcée d'un comportement par défaut▲
Si une classe dérivée choisit de ne pas redéfinir une fonction virtuelle non pure, elle se contente d'hériter du comportement de base par défaut de sa version. Si vous voulez fournir un comportement par défaut, mais que vous ne voulez pas laisser les classes dérivées simplement en hériter "silencieusement", vous pouvez la rendre virtuelle pure tout en lui fournissant un comportement par défaut. L'auteur de la classe dérivée pourra l'appeler s'il veut l'utiliser :
class
B {
public
:
virtual
bool
f() =
0
;
}
;
bool
B::
f() {
return
true
; // c'est un bon comportement par défaut, mais
// il ne devrait pas être utilisé en aveugle
}
class
D : public
B {
public
:
bool
f() {
return
B::
f(); // si D veut le comportement par défaut,
// il doit le dire
}
}
;
II-B-3. Solution de rechange pour les diagnostics de mauvais compilateurs▲
Il y a des situations dans lesquelles il se peut que vous vous retrouviez à appeler accidentellement une fonction virtuelle pure (indirectement à partir d'un constructeur ou destructeur de base ; référez-vous à votre manuel de C++ avancé favori pour avoir des exemples (2) ). Bien sûr, un code bien écrit ne devrait pas présenter ce genre de problème, mais nul n'est parfait et ça peut toujours arriver. Malheureusement, il y a des compilateurs (3) qui ne vous préviendront pas de ce problème. Ceux qui ne le font pas peuvent vous donner de mauvais messages d'erreur qui prennent un temps infini à comprendre. "Argh", criez-vous, quand vous diagnostiquez enfin le problème par vous-même quelques heures plus tard. "Pourquoi le compilateur ne m'a-t-il pas simplement dit ce que c'était ?" Une façon d'éviter une perte de temps pour le débogage de votre programme, consiste à fournir des définitions pour des fonctions virtuelles pures qui ne devraient jamais être appelées, et à introduire de vrais codes malfaisants dans ces définitions qui vous feront immédiatement savoir si vous les avez appelées par accident. Exemple :
class
B {
public
:
virtual
bool
f() =
0
;
}
;
bool
B::
f() {
// ceci ne devrait JAMAIS être appelé
if
( PromptUser( "pure virtual B::f called -- "
"abort or ignore?"
) ==
Abort )
DieDieDie();
}
Dans la classique fonction DieDieDie(), faites ce qu'il faut sur votre système pour accéder au débogueur, ou afficher une pile, ou obtenir d'une manière ou d'une autre des informations de diagnostic. Voici quelques méthodes courantes qui vous feront passer dans le débogueur sur la plupart des systèmes. Prenez celle que vous préférez :
void
DieDieDie() {
// écrit dans un pointeur null
memset( 0
, 1
, 1
);
}
void
DieDieDie() {
// une autre méthode style C
assert( false
);
}
void
DieDieDie() {
// retourne au dernier "catch(...)"
class
LocalClass {}
;
throw
LocalClass();
}
void
DieDieDie() {
// pour les standardophiles
throw
logic_error();
}
Vous saisissez le concept. Soyez créatifs.
III. Remerciements▲
Cet article est une traduction par l'équipe de la rubrique C++ de l'article de Herb Sutter publié sur Guru of the Week. Vous pouvez retrouver cet article dans sa version originale sur le site de Guru of the Week : (Im)pure Virtual Functions.
Merci à Freem et Luc Hermitte pour leur relecture technique, à ClaudeLELOUP et jacques_jean pour leur relecture orthographique.