[20] Héritage — fonctions virtuelles
(Une partie de C++ FAQ Lite fr, Copyright © 1991-2002, Marshall Cline cline@parashift.com )

Traduit de l'anglais par Stéphane Bailliez

Les FAQs de la section [20]


[20.1] Qu'est ce qu'une "fonction membre virtuelle" ou "méthode virtuelle"?

D'un point de vue OO, il s'agit de la plus importante fonctionnalité du C++: [6.8], [6.9].

Une fonction virtuelle permet aux classes dérivées de remplacer l'implémentation fournie par la classe de base. Le compilateur s'assure que la méthode remplacée est bien appelée quand l'objet en question est bien du type de la classe dérivée, même si l'objet est accédé par un pointeur de base plutôt qu'un pointeur dérivé. Cela permet aux algorithmes de la classe de base d'être remplacé dans la classe dérivée, meme si les utilisateurs ne connaissent pas la classe dérivée.

La classe dérivée peut complétement remplacer ("override") les fonctions membres de la classe de base, ou la classe derivée peut partiellement remplacer ("augment") les fonctions membres de la classe de base. Cette dernière action, si elle est désirée,  est obtenue lorsque les fonctions membres de la classe dérivée appellent les fonctions membres de la classe de base.

[ Haut | Bas | Section précédente | Section suivante ]


[20.2] Comment le C++ réalise t-il le lien dynamique(dynamic binding) et le typage statique (static typing)?

[Recently added the definition of polymorphism thanks to Kemberli Jennings (on 1/00).]

Lorsque vous avez un pointeur sur un objet, l'objet peut trés bien être une classe qui est dérivée de la classe du pointeur (e.g. un Vehicle* qui pointe sur un objet Car; cela est appelé le "polymorphisme"). Il y a donc deux types: le type (statique) du pointeur (ici Vehicle), et le type (dynamique) de l'objet qui est pointé (ici Car).

Le typage statique (static typing) signifie que la légalité de l'appel d'une fonction membre est vérifiée le plus tôt possible: lors de la compilation. Le compilateur utilise le type statique du pointeur pour déterminer si l'appel de la fonction membre est légal ou non. Si le type du pointeur permet de peut gérer la fonction membre, alors l'objet pointé peut certainement le gérer également. E.g. si Vehicle a un certain nombre de fonctions membres, alors Car a également ces fonctionnalités car c'est une sorte-de Vehicle.

le lien dynamique (dynamic binding) signifie que l'adresse du code lors de l'appel d'une fonction membre est déterminé au dernier moment possible: basé sur le type dynamic de l'objet lors de l'exécution. On appelle cela le lien dynamique (dynamic binding) car le lien au code qui est appelé est accompli dynamiquement (lors de l'exécution). Le lien dynamique est un résultat des fonctions virtuelles.

[ Haut | Bas | Section précédente | Section suivante ]


[20.3] Quelle est la différence lors de l'appel des fonctions membres virtuelles et non-virtuelles?

Les fonctions membres non-virtualsont résolues statiquement. C'est à dire que la fonction membre est sélectionnée statiquement (durant la compilation) suivant le type du pointeur (ou de la référence) sur l'objet.

Par contraste, les fonctions membres virtual sont résolues dynamiquement (durant l'exécution). C'est à dire que la fonction membre est sélectionnée dynamiquement (durant l'exécution) suivant le type de l'objet et non suivant le type du pointeur ou de la référence sur cet objet. On appelle cela le "dynamic binding". La plupart des compilateurs utilise une variante de la technique suivante: si l'objet a une ou plusieurs fonctions virtual, le compilateur rajoute un pointeur caché dans l'objet appelé "virtual pointer" ou "v-pointer". Ce "v-pointer" pointe sur un table globale appelée "table virtuelle" (virtual table) ou "v-table".

Le compilateur crée une v-table pour chaque classe ayant au moins une fonction virtual. Par exemple si la classe Circle a des fonctions virtual pour draw(), move() et resize()il y aura exactement une v-table associée à la classe Circle, cela quelque soit le nombre d'objets Circle, et le v-pointer de ces objets Circle pointera sur la v-table de Circle. La v-table elle-même à des pointeurs sur chaque fonction virtuelles de la classe. Par exemple, la v-table de Circle aura trois pointeurs: un pointeur sur Circle::draw(), un pointeur sur Circle::move(), et un pointeur sur Circle::resize().

Pendant le dispatch d'une fonction virtuelle, le système récupère la v-table de la classe pointée par le v-pointer de l'objet et récupère ensuite  l'emplacement de la méthode donnée par la v-table.

L'overhead taille de la technique ci dessus est nominal: un pointeur additionnel par objet (mais seulement pour les objets qui ont besoin du dynamic binding), ainsi qu'un pointeur par méthode virtuelle. L'overhead performance est également nominal comparé à l'appel d'un fonction normale, une fonction virtual nécessite deux appels supplémentaires (un pour obtenir la valeur du v-pointer, un deuxième pour obtenir l'adresse de la méthode). Aucun de ces overheads n'existe pour les fonctions non-virtual car le compilateur fait la résolution des fonctions non-virtual à la compilation basé sur le type du pointeur.

Note: La discussion ci-dessus est considérablement simplifiée, car il n'est pas tenu compte de la structure obtenue par l'héritage multiple, l'héritage virtuel, le RTTI, etc., ni ne tient compte des pages faults, des appels de fonctions par un pointeur sur fonction, etc. Si vous souhaitez davantage d'information envoyez un message sur comp.lang.c++; S'IL VOUS PLAIT NE M'ENVOYEZ PAS DE MAILS !

[ Haut | Bas | Section précédente | Section suivante ]


[20.4] Quand le destructeur doit il être virtuel?

Lorsque que vous êtes susceptible de faire un deleted'un objet dérivé par un pointeur de base.

Les fonctions virtual sont liées au code associé à la classe de l'objet, plutôt que à la classe du pointeur ou de la référence.Lorsque vous écrivez delete basePtr, et que la classe de base à un destructeur virtual, le destructeur appelée est celui associé à au type de l'objet  *basePtr, plutôt que celui associé au type du pointeur. Il s'agit généralement d'une bonne chose.

ATTENTION; POUR PERSONNES AVERTIES.
Techniquement parlant, vous avez besoin de déclarer le destructeur de la classe de base virtuel si et seulement si vous pensez permettez à quelqu'un d'appeler le destructeur de l'objet par un pointeur de la classe de base (cela est normalement réalisé implicitement par un delete), et si l'objet à détruire et d'une classe dérivée qui a un destructeur non trivial. Une classe a un destructeur non trivial si elle a ou un destructeur explicitement défini, ou si elle a un obket member ou une classe de base qui a un destructeur non trivial (notez bien qu'il s'agit d'une définition récursive (e.g. une classe a un destructeur non trivial si elle a un objet membre(qui a une classe de base (qui a un objet membre(qui a une classe de base (qui a un destructeur explicitement défini)))))
FIN ATTENTION; POUR PERSONNES AVERTIES

Si vous avez du mal a comprendre la règle précédente, essayez celle ci (ultra)simplifiée en longueur. Une classe doit avoir un destructeur virtuel a moins que cette classe n'ait pas de fonctions virtuelles. Donc: si vous avez des fonctions virtuelles, vous allez certainement effectuer des opérations sur les objets dérivés via le pointeur de base, et certaines de ces opérations peuvent appeler le destructeur (normalement appelé implicitement par delete). De plus, une fois que vous avez mis votre première fonction virtuelle au sein de la classe, vous avez déjà réservé l'espace de stockage supplémentaire par objet (un pointeur par objet, notez que cela est théoriquement spécifique au compilteur; en pratique, tout le monde fait plus ou moins la même chose), donc mettre le destructeur virtuel ne vous coûtera généralement rien de plus.

[ Haut | Bas | Section précédente | Section suivante ]


[20.5] Qu'est ce qu'un "constructeur virtuel" ?

Un idiome qui vous permet de faire ce que le C++ ne supporte pas directement.

Vous pouvez obtenir l'effet d'un constructeur virtuel par l'utilisation d'une fonction membre virtual clone() (construction par copie), ou une fonction membre virtual create()  (pour le constructeur par défaut).

    class Shape {
    public:
      virtual ~Shape() { }                // Un destructeur virtual
      virtual void draw() = 0;            // Une fonction virtuelle pure
      virtual void move() = 0;
       // ...
      virtual Shape* clone()  const = 0;   // Utilises la construction par copie
      virtual Shape* create() const = 0;   // Utilises le constructeur par défaut
    };

    class Circle : public Shape {
    public:
      Circle* clone()  const { return new Circle(*this); }
      Circle* create() const { return new Circle();      }
       // ...
    };

Dans la fonction membre clone(), le code new Circle(*this) appelle le constructeur de copie de Circle pour copier l'état de this dans le nouvel objet Circle. (C'est un clone). Dans la fonction membre create(), le code new Circle() appelle le constructeur par défaut de Circle..

Les clients les utilisent comme s'il s'agissait de "constructeurs virtual":

    void userCode(Shape& s)
    {
      Shape* s2 = s.clone();
      Shape* s3 = s.create();
       // ...
      delete s2;    // Vous avez probablement besoin d'un destructeur virtuel ici.
      delete s3;
    }

Cette méthode fonctionnera correctement que Shape soit un Circle, Square, ou une autre généralisation (kind-of) de Shape qui n'existe pas encore. Il permet donc la création d'un objet du même type que celui que vous traitez sans que vous en ayez connaissance puisque vous travaillez avec le type de base.

Note: Le type de retour de la fonction membre clone() de Circle  est intentionnellement différente du type de retour de la fonction membre clone()de Shape. Cela s'appelle un type de retour covariant (Covariant Return Types), une fonctionnalité qui n'était originellement pas partie intégrante du langage. Si votre compilateur se plaint de la déclaration Circle* clone() const dans la classe Circle (e.g., avec un message du genre "The return type is different" ou "The member function's type differs from the base class virtual function by return type alone"), vous avez un vieux compilateur et vous avez besoin de changer le type de retour en Shape*.

[ Haut | Bas | Section précédente | Section suivante ]


E-mail Marshall Cline Ecrire à l'auteur, au traducteur, ou en savoir plus sur la traduction.
[ C++ FAQ Lite fr | Table des matières | Index | A propos de l'auteur | © | Téléchargez votre propre copie ]
Dernière révision le 12 Nov 2002