Menu

Fonctions virtuelles en C++ : le polymorphisme expliqué

Les fonctions virtuelles permettent à un pointeur de classe de base d'appeler à l'exécution la version de la méthode de la classe dérivée. Découvrez virtual, override, les classes abstraites et pourquoi le destructeur de la base doit être virtuel.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Pourquoi l'héritage seul ne suffit pas

À la page précédente, vous avez construit une hiérarchie de classes : une classe dérivée hérite des membres de sa base. Mais il y a un piège. Lorsque vous appelez une méthode via un pointeur de classe de base, C++ décide quelle fonction exécuter en fonction du type du pointeur, et non du type réel de l'objet. Ainsi, un Animal* qui pointe en réalité vers un Dog appelle quand même la version de la méthode de Animal.

Ce n'est presque jamais ce que vous voulez. La plupart du temps, vous avez une collection de pointeurs de classe de base, chacun pointant vers un objet dérivé différent, et vous voulez que chacun se comporte comme lui-même. Les fonctions virtuelles rendent cela possible.

L'objet est véritablement un Dog, et pourtant a->speak() a exécuté Animal::speak(). Comme speak n'est pas virtuelle, le compilateur a choisi la fonction à la compilation à partir du type statique Animal*. C'est le bug que les fonctions virtuelles existent pour corriger.

Rendre une fonction virtuelle

Ajoutez le mot-clé virtual à la méthode de la classe de base. Désormais, l'appel est résolu à l'exécution en fonction du type réel de l'objet - c'est la répartition dynamique.

Une seule boucle sur des Animal*, trois comportements différents. Le pointeur de base « connaît » le type réel à l'exécution et effectue la répartition en conséquence. Ce mécanisme unique - une interface, plusieurs implémentations - est précisément ce que signifie le polymorphisme en C++.

Notez que virtual ne doit apparaître que sur la déclaration de la base ; une fois qu'une fonction est virtuelle, elle reste virtuelle automatiquement dans toutes les classes dérivées. La réécrire dans la classe dérivée est facultatif et redondant.

Utilisez toujours le mot-clé override

Dans l'exemple ci-dessus, chaque méthode dérivée est marquée override. C'est facultatif pour que le code fonctionne, mais vous devriez le considérer comme obligatoire. override (C++11) demande au compilateur de vérifier que vous redéfinissez bien une fonction virtuelle de la base avec une signature correspondante. Si vous vous trompez subtilement sur la signature, vous obtenez une erreur claire au lieu d'un bug silencieux.

struct Animal {
    virtual void speak() const { }   // note : const
};

struct Dog : Animal {
    void speak() { }            // PAS const - c'est une NOUVELLE fonction, pas une redéfinition !
    void speak() override { }   // erreur : 'speak' ne redéfinit rien - vous le dit immédiatement
};

Sans override, le premier speak() compile sans problème mais n'est jamais appelé via un Animal*, car sa signature diffère de celle de la base (il manque const). Vous passeriez un après-midi entier à vous demander pourquoi votre redéfinition ne fait rien. Avec override, le compilateur détecte l'incohérence sur-le-champ. Ajoutez-le à chaque fonction qui redéfinit.

Fonctions virtuelles pures et classes abstraites

Parfois, la classe de base n'a pas de valeur par défaut sensée - quel son émet un « Animal » générique ? Dans ce cas, déclarez la fonction virtuelle pure en lui affectant = 0. Cela la laisse sans corps et transforme la classe en une classe abstraite qui ne peut pas être instanciée seule. Elle n'existe que pour définir une interface que les classes dérivées doivent satisfaire.

Chaque sous-classe concrète doit implémenter area(), sinon elle reste abstraite elle aussi. C'est ainsi que C++ exprime les « interfaces » : une classe abstraite ne contenant que des fonctions virtuelles pures est l'équivalent C++ d'une interface dans des langages comme Java.

La règle du destructeur virtuel

C'est le piège qui attrape tout le monde au moins une fois. Lorsque vous faites delete sur un objet via un pointeur de classe de base, C++ appelle le destructeur qu'il trouve - et si ce destructeur n'est pas virtuel, il n'exécute que le destructeur de la base. La partie dérivée n'est jamais détruite, laissant fuir tout ce qu'elle possédait. La norme qualifie cela de comportement indéfini.

La correction tient en un mot : rendez le destructeur de la base virtual. Alors delete p exécute d'abord ~Derived, puis ~Base, exactement comme il se doit.

struct Base {
    virtual ~Base() { cout << "~Base\n"; }   // correct
};
// maintenant : ~Derived puis ~Base

Règle empirique : dès qu'une classe possède une fonction virtuelle, donnez-lui aussi un destructeur virtuel. Si une classe est destinée à être une classe de base utilisée via des pointeurs, son destructeur doit être virtuel.

Erreurs courantes et pièges

Encore quelques pièges à surveiller une fois à l'aise avec les fonctions virtuelles :

Le découpage d'objet (object slicing). Si vous passez ou stockez un objet dérivé par valeur dans une variable de la base, la partie dérivée est « découpée » et il vous reste un simple objet de base - la répartition virtuelle n'atteint plus la redéfinition. Utilisez toujours des pointeurs ou des références pour le polymorphisme :

Dog d;
Animal a = d;   // DÉCOUPÉ : a n'est plus qu'un Animal, la partie Dog a disparu
a.speak();      // exécute Animal::speak même si virtuelle

Animal& ref = d;   // OK : la référence conserve le type réel
ref.speak();       // exécute Dog::speak

N'appelez pas de fonctions virtuelles depuis les constructeurs ou les destructeurs. Pendant la construction, la partie dérivée n'existe pas encore, donc un appel virtuel se résout vers la version de la classe courante, et non vers la redéfinition dérivée - rarement ce que vous comptez faire.

La répartition virtuelle a un petit coût. Chaque appel virtuel passe par une table cachée de pointeurs de fonction (la « vtable »), une indirection par appel. C'est peu coûteux, mais pas gratuit, alors ne rendez pas une fonction virtuelle si vous n'avez pas réellement besoin de la redéfinir.

Appeler la version de base volontairement. À l'intérieur d'une redéfinition, vous pouvez toujours invoquer explicitement l'implémentation de la base avec Base::method() - utile lorsque le comportement dérivé étend plutôt qu'il ne remplace celui de la base.

Suite : la surcharge d'opérateurs

Les fonctions virtuelles permettent à vos objets de personnaliser leur comportement via une interface partagée. La page suivante montre comment personnaliser les opérateurs qui agissent sur vos objets : avec la surcharge d'opérateurs, vous pouvez apprendre à vos propres types à réagir à +, ==, <<, et plus encore, de sorte que Vector + Vector ou cout << myObject se lisent aussi naturellement que pour les types intégrés.

Questions fréquentes

Qu'est-ce qu'une fonction virtuelle en C++ ?

Une fonction virtuelle est une fonction membre déclarée avec le mot-clé virtual dans une classe de base, de sorte que lorsque vous l'appelez via un pointeur ou une référence de la classe de base, C++ exécute la redéfinition de la classe dérivée au lieu de la version de la base. Cette sélection à l'exécution s'appelle la répartition dynamique (dynamic dispatch) et constitue le fondement du polymorphisme.

Quelle est la différence entre une fonction virtuelle et une fonction virtuelle pure ?

Une fonction virtuelle possède un corps et peut être redéfinie. Une fonction virtuelle pure est déclarée avec = 0 et n'a pas de corps dans la classe de base - elle oblige chaque classe dérivée concrète à fournir une implémentation. Toute classe possédant au moins une fonction virtuelle pure est une classe abstraite et ne peut pas être instanciée.

Pourquoi une classe de base a-t-elle besoin d'un destructeur virtuel en C++ ?

Si vous faites delete sur un objet dérivé via un pointeur de classe de base et que le destructeur de la base n'est pas virtuel, seul le destructeur de la base s'exécute - la partie dérivée n'est jamais nettoyée, ce qui provoque des fuites de ressources et constitue un comportement indéfini. Déclarez virtual le destructeur de toute classe destinée à être utilisée de façon polymorphe.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER