Guru Of the Week n° 31 : les fonctions virtuelles (im)pures

| Herb Sutter | Traduction par l'équipe de la rubrique C++ |

Difficulté : 7 / 10
Est-ce toujours une bonne chose de faire une fonction virtuelle pure, tout en fournissant un body ?

Retrouvez l'ensemble des Guru of the Week sur la page d'index.

N'hésitez pas à commenter cet article ! Commentez

- Titre : Guru Of the Week n° 31 : les fonctions virtuelles (im)pures

- Auteurs : Herb Sutter, Traduction par l'équipe de la rubrique C++

- Parution : 21 juin 2012

- Licence : Copyright ® 2009 Herb Sutter. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Problèmes

- I-A. Question Junior

- I-B. Question Guru

- II. Solutions

- II-A. Question Junior

- II-B. Question Guru

- II-B-1. Destructeur virtuel pur

- II-B-2. Acceptation consciente forcée d'un comportement par défaut

- II-B-3. Solution de rechange pour les diagnostics de mauvais compilateurs

- III. Remerciements

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.

(1)Note de traduction : vous pouvez également consulter la FAQ Pourquoi et quand faut-il créer un destructeur virtuel ?

(2)Note de traduction : vous pouvez également consulter la FAQ Puis-je appeler des fonctions virtuelles dans le constructeur (ou le destructeur) ?

(3) C'est vrai que techniquement, c'est l'environnement de l'application qui détecte ce genre de chose. Toutefois je dirai à chaque fois "compilateur", parce que c'est généralement le compilateur qui est censé entrer dans le code qui vérifie cela pour vous quand vous lancez le programme.