Quoi ! l'Ours Blanc des Carpathes ose agglomérer en un seul chapitre ces deux notions si importantes du modèle objet ? en effet, cela peut paraître hérétique, mais il faut savoir que contrairement à d'autres langages, la forme forte du polymorphisme est limitée, en C++, aux classes d'une même arboresence, ce qui explique sans l'excuser l'organisation de ce poly.
Plan du chapitre :
Liste des figures :
Liste des programmes :
Liste des tableaux :
Le C++ propose une implémentation très complète de lhéritage car il propose aussi bien lhéritage simple que multiple ainsi que des options avancées dhéritage sélectif des attributs aussi bien que des méthodes. Signalons également comme aspects positifs le chaînage automatique des constructeurs et destructeurs ou la possibilité de gérer (relativement) facilement lhéritage à répétition.
Je ne regretterais que labsence dinterfaces au sens Java ou Objective C du terme. Notion quil est toutefois possible d'assez bien les simuler en utilisant des classes virtuelles pures sans attribut.
Exemples :
Déclaration | Commentaire |
---|---|
Héritage simple : Cercle dérive d'ObjetGraphique en mode public | |
Héritage multiple, (double en fait) : TexteGraphique dérive à la dois d'ObjetGraphique et de Chaine et ce, à chaque fois en public. | |
Pile hérite uniquement de la classe Vecteur mais en private. |
Tableau 5.1 : quelques exemples d'héritage
Un attribut protected nest pas accessible à lextérieur de la classe mais lest aux classes dérivées. Ce modificateur est donc intermédiaire entre private et public. A linstar de private, il respecte le principe dencapsulation tout en favorisant la transmission des attributs entre classe mère et classe fille. Le tableau suivant récapitule les accès fournis par ces trois modificateursgoss :
Modificateur d'accès | Visibilité dans les classes filles | Visibilité depuis l'extérieur |
---|---|---|
private | Non | Non |
protected | Oui | Non |
public | Oui | Oui |
Tableau 5.2 accès aux membres et modificateurs
Le modificateur d'héritage peut être public ou private. Il conditionne la visibilité des membres de la classe mère dans la classe dérivée. Le tableau suivant indique à quel accès est associé un membre de la classe mère dans la classe fille en fonction du modificateur dhéritage :
Accès dans la classe mère | Accès dans la classe fille | |
---|---|---|
Héritage public | Héritage private | |
private | Non accessible | Non accessible |
protected | protected | private |
public | public | private |
Tableau 5.3 Effet du modificateur d'héritage sur les modificateurs d'accès
Reprenons maintenant la définition classique de lhéritage :
La classe dérivée est une forme
spécialisée de sa classe mére.
Exprimé ainsi, on peut déduire :
La classe mère transmet à sa classe
fille tous ces membres, attributs et méthodes.
En effet, si un objet de la classe dérivée est une forme spécialisée dun objet de la classe mère, il est logique quil puisse utiliser directement les attributs de la classe mère, donc, de ce point de vue, les attributs doivent être placés en protected et lhéritage en mode public.
Nous avons vu que lhéritage en public et lutilisation du modificateur daccès protected traduit la notion classique dhéritage. Quel est donc lutilité du modificateur dhéritage private qui cache à lutilisateur de la classe dérivée tous les membres de la classe mère ?
Considérons la relation Est implémenté sous forme de au travers dun exemple classique.
Soit la classe Pile. On peut limplémenter à laide dun tableau, dune liste chaînée ou de toute autre classe universelle de stockage déléments. A ce titre, les utilisateurs de la classe pile ne doivent pas avoir accès aux membres de la classe de stockage. Ce qui conduit certains auteurs à proposer la solution technique suivante :
class Pile : private ClasseDeStockage { // membres de la classe Pile };
Etudions les avantages et les inconvénients dune telle implémentation :
De mon point de vue, la rigueur conceptuelle doit toujours primer sur leffacité du code généré. Aussi, lhéritage, quil soit public ou private doit toujours rendre compte dune relation de généralisation / spécialisation. Aussi, je nutilise pas lhéritage en private pour traduire la relation Est implémenté sous forme de. Pour cela jutilise de lagrégation.
En effet, je pourrais écrire la classe Pile ainsi :
class Pile { private: ClasseDeStockage elements; }
Bien entendu, il nest plus possible dutiliser les attributs non private de ClasseDeStockage dans les méthodes de Pile, néanmoins, si la classe ClasseDeStockage est bien pensée, cela ne devrait pas poser de problème spéficique.
Mais surtout, point extrèmement positif : il nest pas incohérent de dire :
Une pile contient un objet de ClasseDeStockage
pour stocker ces éléments
En outre, et cest un indicateur certain, la Librairie standard du C++ prône elle aussi lutilisation de lagrégation pour la relation Est implémenté sous forme de.
Rappelons tout dabord quil est inconcevable de déclarer en public un attribut pour respecter le principe dencapsulation des objets. Il nous reste donc la possibilité de les déclare protected ou private.
Daucuns vous diront quil faut toujours déclarer protected un attribut car cela permet de le rendre accessible dans les classes dérivées (sous réserve de faire de lhéritage en mode public).
A mon avis, certains attributs doivent néanmoins rester en private afin de garantir, en particulier, le respect de certaines contraintes dintégrité. Par exemple, considérons une classe Hélicoptère avec 2 attributs qui sont langle dincidence (linclinaison en avant ou en arrière de lhélicoptère) et sa vitesse. La vitesse est calculée en fonction de divers paramètres, dont, en particulier lincidence.
Dans cette classe Hélicoptère, vous fournissez une méthode nommée modifierIncidence qui permet outre la modification de l'angle d'incidence, de mettre automatiquement à jour la vitesse.
Dérivons maintenant en mode public cette classe Hélicoptère. Si lattribut était en protected, alors, la classe dérivée aura visibilité directe dessus, ce qui lui permettra de modifier la valeur de lattribut incidence sans passer par la méthode modifierIncidence ce qui ne garantit plus lintégrité de la vitesse.
Cet exemple parmi tant dautres me conduit à minterroger sur le bien fondé de placer tous les attributs en protected. Aussi, je me suis érigé une règle qui mest propre :
Ne placer en protected que les attributs qui
pourront être modifiés individuellement dans les classes
dérivées sans danger pour l'intégrité totale de
l'objet
Je laisse toujours les autres attributs en private, en proposant en inline des méthodes daccès en lecture / écriture garantissant l'intégrité de l'objet lorsque cela savère nécessaire.
En outre, laisser des attributs en private permet de faire de faire de lhéritage sélectif.
La règle est simple :
Le constructeur dune classe
dérivée appelle toujours les constructeurs de ses classes
mères avant de construire ses propres attributs. De même, les
destructeurs des classes mères sont automatiquement appelés par
le destructeur de la classe fille.
Ainsi, deux grands cas s'opposent :
Prenons le cas de la classe ObjetGraphique telle qu'elle a été définie ci-dessus. Nous allons dériver deux nouvelles classes respectivement nommées Ligne et Cercle. Pour l'instant, nous ne spécifions que les constructeurs et les attributs. La classe Cercle rajoute un attribut nommé rayon alors que la classe Ligne rajout un angle et une longueur. Les codes résultants sont :
classe ObjetGraphique { public: ObjetGraphique(int x, int y, int couleur=0, int epaisseur=0) : pointBase_(x,y), couleur_(couleur), epaisseur_(epaisseur) {} protected: Point pointBase_; int couleur_; int epaisseur_; }; class Ligne : public ObjetGraphique { public: Ligne (int x, int y, int longueur, double angle, int couleur=0, int epaisseur=0) : ObjetGraphique(x, y, couleur, epaisseur), longueur_(longueur), angle_(angle) {} ... private: int longueur_; double angle_; }; class Cercle : public ObjetGraphique { public: Cercle (int x, int y, int rayon, int couleur=0, int epaisseur=0) : ObjetGraphique(x, y, couleur, epaisseur), rayon_(rayon) {} ... private: int rayon_; }
Programme 5.1 Construction des objets dérivés
Première chose que l'on remarque immédiatement : les attributs présents dans la classe ObjetGraphique n'ont pas à être redéclarés dans les classes dérivées. Etant à accès protected, ils seront transférés immédiatement vers les classes filles.
L'appel au constructeur de la classe mère se fait en première position dans la liste d'initialisation, avant l'appel des constructeurs des attributs. Cette stratégie est cohérente : on commence par initialiser les attributs en provenance de la classe mère avant d'initialiser les derniers introduits.
Dans le cas de l'héritage multiple, on doit appeler les constructeurs dans l'ordre de dérivation. Par exemple, supposons que l'on souhaite créer une classe TexteGraphique dérivant à la fois des classes ObjetGraphique et Chaine dont nous disposons déjà. Une partie du code pourrait être :
class TexteGraphique : public ObjetGraphique, public Chaine { public: TexteGraphique(int x, int y, int couleur, const char *sz, const char *fontName="Verdana", unsigned shor fontSize=12) : ObjetGraphique(int x, int y, int couleur, 0), Chaine(sz) { // Création d'une fonte dans un systeme imaginaire // code purement demonstratif laFonte=System.GetFontByName(fontName); laFonte->setSize(fontSize); } ~TexteGraphique() { delete laFonte; } private: Font *laFonte; };
Programme 5.2 Héritage multiple
Cet exemple est intéressant à plusieurs titres :
Par défaut, en C++, une méthode nest pas polymorphe (ce qui, à mon avis, va à lencontre des principes Objet). Afin de la rendre polymorphe, il faut respecter deux principes essentiels :
Attention ! une particularité du C++ impose que toute classe possédant au moins une méthode virtuelle doit également avoir un destructeur virtuel.
Appliquons ce principe au cas des objets graphiques. Quelle est la première caractéristique d'un objet graphique ? s'afficher parbleu ! aussi, chaque classe d'objets graphiques va disposer d'une méthode nommée afficher. Dans notre cas, nous allons considérer que l'affichage d'un objet consiste à envoyer ses caractéristiques en mode texte sur l'écran.
En outre, considérons la classe ObjetGraphique. Avez vous déjà rencontré des objets graphiques génériques dans votre vie ? personnellement moi jamais (ou alors, j'ai jamais été assez plein pour ca !). En revanche, j'ai régulièrement croisé des lignes, des cercles, des rectangles et même des triangles qui sont autant de manifestations du concept d'objet graphique. Vous m'avez sans doute entendu arriver avec mes gros sabots : ObjetGraphique est l'illustration typique d'une classe abstraite représentative d'un concept. Elle sera ensuite dérivée en classes concrètes, ici Cercle et Ligne.
Reprenons : ObjetGraphique est une classe abstraite ; elle possède donc surement des méthodes abstraites ! L'exemple typique est ici afficher. En effet, l'on ne saurait afficher un objet graphique générique, alors que l'affichage d'un objet de classe Cercle consistera à tracer un rond et celui de Ligne au dessin d'un segment.
La syntaxe de déclaration d'une méthode abstraitre est la suivante :
Le modificateur virtual introduit une méthode virtuelle (qu'elle soit abstraite ou non). C'est le =0 qui traduit le caractère abstrait. Dans le vocable C++, on parle plutôt de méthode virtuelle pure.
Afin de nous simplifier l'existence, nous allons ici considérer que l'affichage d'un objet consiste à envoyer ses caractéristiques en mode texte sur l'écran, nous obtenons alors (après ajout de fonctionnalités dans la classe ObjetGraphique) le code suivant :
//-------------------------------------------------------------------------------- // Fichier ObjetGraphique.hxx //-------------------------------------------------------------------------------- #ifndef __Objet_Graphique_HXX__ #define __Objet_Graphique_HXX__ #include "Point.hxx" class ObjetGraphique { public: // Une chtite constante :) enum { COULEURFOND=0 }; public : ObjetGraphique(int x, int y, int couleur=0, int epaisseur=0) : pointDeBase_(x,y), couleur_(couleur), epaisseur_(epaisseur) { NbObjetsGraphiques_++; } const Point &pointDeBase(void) const { return pointDeBase_; } int couleur(void) const { return couleur_; } int epaisseur(void) const { return epaisseur_; } virtual void afficher(void) const=0 ; // methode virtuelle pure a redefinir dans les sous classes virtual void effacer(void) // Comportement par defaut de l'effacage { int sauveCouleur=couleur_; couleur_=COULEURFOND; afficher(); couleur_=sauveCouleur; } static int NbObjetsGraphiques(void) { return NbObjetsGraphiques_; } void deplacerVers(int versX, int versY) { effacer(); pointDeBase_.deplacerVers(versX, versY); afficher(); } void deplacerDe(int surX, int surY) { deplacerVers(pointDeBase_.x()+surX, pointDeBase_.y()+surY); // Reutilisation du code de la fonction precedente pour fiabilisation } virtual ~ObjetGraphique(void) { NbObjetsGraphiques--; } // Donnees protected : accessibles aux sous classes ! protected : Point pointDeBase_; int couleur_; int epaisseur_; static int NbObjetsGraphiques_; }; #endif //-------------------------------------------------------------------------------- // Le fichier ObjetGraphique.cc est vide //-------------------------------------------------------------------------------- //-------------------------------------------------------------------------------- // Fichier Ligne.hxx //-------------------------------------------------------------------------------- #ifndef __Ligne_HXX__ #define __Ligne_HXX__ #include "ObjetGraphique.hxx" class Ligne : public ObjetGraphique { public : Ligne(int x, int y, int longueur, double angle, int couleur=0, int epaisseur=0) : ObjetGraphique(x,y,couleur,epaisseur), longueur_(longueur), angle_(angle) { }; virtual void afficher(void) const ; // Redéfinition du code d'affichage d'une ligne virtual ~Ligne(void) {}; private : int longueur_; double angle_; }; #endif //-------------------------------------------------------------------------------- // Fichier Cercle.hxx //-------------------------------------------------------------------------------- #ifndef __Cercle_HXX__ #define __Cercle_HXX__ #include "ObjetGraphique.hxx" class Cercle : public ObjetGraphique { public : Cercle(int x, int y, int rayon, int couleur=0, int epaisseur=0) : ObjetGraphique(x,y,epaisseur,couleur), rayon_(rayon) { }; virtual void afficher(void) const; // Redéfinition du code d'affichage d'un cercle virtual ~Cercle() {}; private : int rayon_; }; #endif //-------------------------------------------------------------------------------- // Fichier Cercle.cc //-------------------------------------------------------------------------------- #include "Cercle.hxx" #include <iostream> using namespace std; void Cercle::afficher(void) const { cout << "Affichage d'un cercle" << endl; cout << "Coordonnées du point de base :" <<endl; cout << " abscisse : " << base().x() << endl; cout << " ordonnée : " << base().y() << endl; cout << "Attributs de trait :" << endl; cout << " couleur : " << couleur_ << endl; cout << " epaisseur : " << epaisseur_ << endl; cout << "Attribut spécifique au Cercle :" << endl; cout << " rayon : " << rayon_ << endl; } //-------------------------------------------------------------------------------- // Fichier Ligne.cc //-------------------------------------------------------------------------------- #include "Ligne.hxx" #include <iostream> using namespace std; void Ligne::afficher(void) const { cout << "Affichage d'une ligne" << endl; cout << "Coordonnées du point de base :" <<endl; cout << " abscisse : " << base().x() << endl; cout << " ordonnée : " << base().y() << endl; cout << "Attributs de trait :" << endl; cout << " couleur : " << couleur_ << endl; cout << " epaisseur : " << epaisseur_ << endl; cout << "Attributs spécifiques à la Ligne :" << endl; cout << " angle : " << angle_ << endl; cout << " longueur : " << longueur_ << endl; }
Programme 5.3 Mise en place du polymorphisme
Le code suivant instancie une ligne et un cercle puis appelle la méthode afficher sur chacun d'entre eux. A ce moment là, le type de chaque objet est parfaitement connu :
#include "ObjetGraphique.hxx" #include "Ligne.hxx" #include "Cercle.hxx" int main(int, char **) { Ligne uneLigne(10,20,150, 0.35, 0, 2); Cercle unCercle(50,50, 30); uneLigne.afficher(); cout << endl; unCercle.afficher(); return 0; }
Programme 5.4 Appel polymorphique sur objets de type connu
dont le résultat est :
Affichage d'une ligne Coordonnées du point de base : abscisse : 10 ordonnée : 20 Attributs de trait : couleur : 0 epaisseur : 2 Attributs spécifiques à la Ligne : angle : 0.35 longueur : 150 Affichage d'un cercle Coordonnées du point de base : abscisse : 10 ordonnée : 20 Attributs de trait : couleur : 0 epaisseur : 2 Attribut spécifique au Cercle : rayon : 30
Figure 5.1 Resultat d'un appel polymorphique simple
Plus intéressant, utilisons du vrai polymorphisme en créant un tableau de 2 pointeurs sur ObjetGraphique. « Hep la ! C'est une classe abstraite ObjetGraphique, pas question de créer des instances » allez vous me répondre. Du calme ! je ne crée pas d'instances mais des pointeurs qui eux vont désigner des objets des classes dérivées comme le montre le code suivant :
#include "ObjetGraphique.hxx" #include "Ligne.hxx" #include "Cercle.hxx" int main(int, char **) { Ligne uneLigne(10,20,150, 0.35, 0, 2); Cercle unCercle(50,50, 30); ObjetGraphique *tab[2]; tab[0]=&unCercle; tab[1]=&uneLigne; for (int i=0;i<2;i++) { tab[i]->afficher(); cout << endl; } return 0; }
Programme 5.5 : Appel polymorphique sur tableau de pointeurs
Le résultat obtenu est le suivant :
Affichage d'un cercle Coordonnées du point de base : abscisse : 10 ordonnée : 20 Attributs de trait : couleur : 0 epaisseur : 2 Attribut spécifique au Cercle : rayon : 30 Affichage d'une ligne Coordonnées du point de base : abscisse : 10 ordonnée : 20 Attributs de trait : couleur : 0 epaisseur : 2 Attributs spécifiques à la Ligne : angle : 0.35 longueur : 150
Figure 5.2 Resultat de l'appel polymorphique sur tableau de pointeurs
Du fait de son caractère polymorphe, la bonne version de la méthode afficher est invoquée au moment de l'exécution. C'est ce que l'on appelle de la liaison différée ou late binding. Ca ne marche pas par magie, loin de la. En fait, les méthodes virtuelles de chaque classe sont logées dans ce que l'on appelle la table des méthodes virtuelles ou vmt.
L'idée de base des tables de méthodes virtuelles est de toujours ranger les méthodes virtuelles dans le même ordre de manière à pouvoir les appeler par leur position dans la table.
Supposons, par exemple, que la classe A déclare 4 méthodes virtuelles respectivement nommées poly1, poly2, poly3 et poly4. Ses sous classes B et C vont les redéfinir et éventuellement en rajouter. La figure suivante illustre ces trois classes, leurs tables de méthodes virtuelles obtenues ainsi que 4 objets avec leurs pointeurs vers les tables de méthodes virtuelles respectives :
Figure 5.3 Les tables de méthodes virtuelles
Ainsi, l'appel à la méthode poly2 ne s'effectue pas directment mais via la TVM. L'appel devient donc :
Ainsi, cela vous permet de manipuler indistictement des entités regroupées dans un conteneur car l'appel aux méthodes virtuelles est sur d'aboutir sur la bonne méthode.
Reprenons l'exemple de code précédent. Vous aurez remarqué que les méthodes d'affichage des classes Ligne et Cercle partagent une grande partie de code. Ceci est à la fois innefficace et dangereux comme l'est toute duplication de code. Il serait agréable de réutiliser du code placé dans la classe ObjetGraphique ! Aussi, et bien que nous souhaitions que la classe ObjetGraphique reste abstraite, on peut définir le code suivant :
void ObjetGraphique::afficher(void) const { cout << "Affichage des parties communes a tous les objets graphiques" << endl; cout << "Coordonnées du point de base :" <<endl; cout << " abscisse : " << base().x() << endl; cout << " ordonnée : " << base().y() << endl; cout << "Attributs de trait :" << endl; cout << " couleur : " << couleur_ << endl; cout << " epaisseur : " << epaisseur_ << endl; }
Programme 5.6 : Factorisation du code dans la classe mère
Les afficheurs de Ligne et Cercle deviennent alors :
void Cercle::afficher(void) const { cout << "Affichage d'un cercle" << endl; ObjetGraphique::afficher(); cout << "Attribut specifique au Cercle" << endl; cout << " rayon : " << _rayon; } ... void Ligne::afficher(void) const { cout << "Affichage d'une ligne" << endl; ObjetGraphique::afficher(); cout << "Attributs spécifiques à la Ligne :" << endl; cout << " angle : " << angle_ << endl; cout << " longueur : " << longueur_ << endl; }
Programme 5.7 Réutilisation du code de la classe mère
Vous notez que l'on fait appel au code de la classe mère via un appel qui ressemble comme 2 gouttes d'eau à celui d'une méthode de classe. Une fois de plus, la syntaxe du C++ n'est pas cohérente.
En fait, Coplien suggère la représentation suivante pour toute redéfinition d'une méthode virtuelle :
On pourra alors lécrire ainsi :
class B::poly(typeParam1 param1, typeParam2 param2) { PRE(paramètres spécifiques) A::poly(param1, param2); POST(paramètres spécifiques) }
Programme 5.8 Forme canonique de Coplien des méthodes virtuelles
Ce formalisme s'appelle habituellement : forme canonique de Coplien des méthodes virtuelles.
Cette implémentation présente les nombreux avantages liés à la réutilisation de code, en particulier la fiabilisation de limplémentation et lévolutivité (une modification de la classe mère sera répercutée directement dans B).
Vous aurez sans doute noté quelque chose de particulier : en C++ il est possible de fournir du code pour la méthode abstraite afficher de la classe abstraite ObjetGraphique.
Dans ce cas, la redéfinition de la méthode poly prend le nom de programmation différentielle.
L'héritage multiple pose de sérieux problèmes :
Supposons que la classe C hérite à la fois des classes A et B. Il peut y avoir des problèmes de gestion des homonymes dans les cas suivants :
Dans les deux cas la gestion est la même : on utilise le nom de la classe mère en préfixe afin de spécifier lorigine du membre requis. Par exemple, supposons que les deux classes A et B possèdent un attribut nommé attr. Si vous souhaitez les utiliser dans une méthode de la classe C, vous devrez spécifier A::attr ou B::attr. Si vous essayez d'utiliser simplement attr, le compilateur vous informera que ce nom est ambigu.
L'héritage virtuel est un mécanisme destiné à réparer les inconvénients liés à l'héritage à répétition. La figure suivante montre un cas d'héritage à répétition. Rappelons brièvement les problèmes qu'il pose
L'héritage se fait en mode virtuel si le mot clef virtual est présent dans le modificateur d'héritage. L'exemple suivant montre comment utiliser l'héritage virtuel pour résoudre les problèmes liés à l'héritage à répétition :
// Classe de base du système class A { public: A () { cout << "Constructeur de A" << endl; } private: int a; }; // Sous classe directe de A class B : virtual public A { public: // Le constructeur de B appelle celui de A // afin d'initialiser correctement les attributs de A // présents dans la classe B B() : A() { cout << "Constructeur de B" << endl; }; private: int b; }; // Sous classe directe de A class C : virtual public A { public: // Le constructeur de C appele celui de A // afin d'initialiser correctement les attributs de A // présents dans la classe C C() : A () { cout << "Constructeur de C" << endl; }; private: int c; }; // Sous classe de B et C // Afin de gérer l'héritage à répétition, on introduit également A dans la liste // des super classes class D : virtual public A, public B, public C { public: D() : A (), B (), C () { cout << "Constructeur de D" << endl; }; private: int d; };
Programme 5.9 : Mise en place de l'héritage virtuel
En plus de l'héritage double sur les classes B et C, ce code ajoute explicitement la classe A en super classe de D. En outre, A doit arriver en première position dans la liste des super classes et l'héritage sur A doit de nouveau être virtuel. Cette construction garantit que :
Remarque : l'ordre des modificateurs virtual et public dans un héritage virtuel est sans conséquence.
Vous voulez une preuve ? Allez, je suis bon prince, en voici une, minimaliste mais suffisante. Le code d'utilisation des classes précédentes est le suivant :
int main(int, char **) { B b1; cout << endl; C c1; cout << endl; D d1; return 0; }
Programme 5.10 : Essai de l'héritage virtuel
Et voici le résultat commenté :
Constructeur de A // Le constructeur de B (pour l'objet b1) appelle bien celui de A Constructeur de B Constructeur de A // Le constructeur de C (pour l'objet c1) appelle bien celui de A Constructeur de C Constructeur de A // Le constructeur de D appelle une fois le constructeur de A Constructeur de B // une fois le constructeur de B qui n'appelle celui de A du fait de l'héritage virtuel Constructeur de C // une fois le constructeur de C qui n'appelle celui de A du fait de l'héritage virtuel Constructeur de D // puis finalement son code propre !
Quand je vous le disais que le constructeur de A ne serait appelé qu'une seule fois ! Les attributs placés dans les classes ne sont pas mis à contribution par cet exemple. En revanche, ils deviendront d'une importance cruciale dans l'expérience proposée à la fin de cette section.
Tout ceci semble trop parfait pour être honnête, et, bien entendu, il va falloir en payer les conséquences. Tout d'abord, il vous faut savoir qu'il est absolument interdit de faire du transtypage descendant à travers un héritage virtuel. Par exemple :
A *p; B *q; ... q=(B *)a;
est illégal. Soit dit entre nous, il est rarement légitime de faire des transtypages descendants, et si vous en avez besoin, c'est qu'il y a probablement des incohérences dans votre modèle objet.
En outre, et sans vouloir rentrer dans les détails internes de codage du C++ (voir à ce sujet l'ouvrage "Le modèle orienté objet du C++" de l'inévitable et excellent Stanley Lippman"), l'héritage virtuel alourdit considérablement la gestion de la table des méthodes virtuelles et des attributs, il faut donc l'utiliser uniquement à bon escient.
Supposons que vous fournissiez une bibliothèque dont toutes les classes dérivent d'un ancètre commun. Est il nécessaire d'utiliser de l'héritage virtuel afin qu'un de vos client puisse faire de l'héritage multiple sans danger ?
Tout d'abord, il vous faut décider si vos classes ont une chance d'être dérivées par votre client et si oui, peut-il désirer les associer à une autre classe de votre bibliothèque ? si la réponse est oui, alors vous devez prévoir de l'héritage virtuel, dans tous les autres cas, utilisez de l'héritage "normal".
Dans le code précédent, retirez chacun à son tour les modificateurs virtuels dans l'héritage ou l'héritage direct sur A dans la classe D afin de voir quels messages sont générés par votre compilateur préféré. Par exemple, l'oubli du mot clef virtual dans la dérivation de C depuis A provoque respectivement le message de compilation (avec gcc) et l'affichage d'exécution suivants :
essvirt.cc:62: warning: direct base `A' inaccessible in `D' due to ambiguity
Constructeur de A // Le constructeur de B (pour l'objet b1) appelle bien celui de A Constructeur de B Constructeur de A // Le constructeur de C (pour l'objet c1) appelle bien celui de A Constructeur de C Constructeur de A // Le constructeur de D appelle une fois le constructeur de A Constructeur de B // une fois le constructeur de B qui n'appelle celui de A du fait de l'héritage virtuel Constructeur de A // comme C n'hérite pas virtuellement de A, le constructeur de C appelle celui de A // sur sa copie des attributs de A Constructeur de C // appel du constructeur de C Constructeur de D // puis finalement son code propre !
Figure 5.4 Expérience sur l'héritage virtuel
Le message de compilation de gcc étant clairement du à la duplication des attributs de A dans la classe D !