V L’héritage et le polymorphisme en C++

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 :

  1. Généralités
  2. Syntaxe générale
  3. Le rôle du modificateur d’héritage et le modificateur d’accès protected
    1. A quoi sert l'héritage en private ?
    2. Faut il toujours mettre les attributs en protected?
  4. Construction / destruction et héritage
  5. Polymorphisme et classes abstraites
    1. Mise en place du polymorphisme
    2. Exploitation du polymorphisme
    3. Les tables de méthodes virtuelles
    4. La réutilisation du code de la classe mère
  6. Les difficultés liées à l'héritage multiple
    1. Les homonymes
    2. L'héritage virtuel comme solution à l'héritage à répétition
      1. Motivation
      2. Mise en oeuvre
      3. Conséquences néfases de l'héritage virtuel
      4. Quand doit on utilise de l'héritage virtuel ?
      5. Expériences sur l'héritage virtuel

Liste des figures :

  1. Figure 5.1 : Résultat d'appel polymorphique simple
  2. Figure 5.2 : Résultat d'appel polymorphique sur tableau de pointeurs
  3. Figure 5.3 : Les tables de méthodes virtuelles
  4. Figure 5.4 : Expérience sur l'héritage virtuel

Liste des programmes :

Liste des tableaux :

  1. Tableau 5.1 : Quelques exemples d'héritage
  2. Tableau 5.2 : Accès aux membres et modificateurs
  3. Tableau 5.3 : Effet du modificateur d'héritage sur les modificateurs d'accès

5.1 Généralités

Le C++ propose une implémentation très complète de l’héritage car il propose aussi bien l’héritage simple que multiple ainsi que des options avancées d’hé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 l’héritage à répétition.

Je ne regretterais que l’absence d’interfaces au sens Java ou Objective C du terme. Notion qu’il est toutefois possible d'assez bien les simuler en utilisant des classes virtuelles pures sans attribut.

5.2 Syntaxe générale :

NomClasseDérivée : [public|private] ClasseBase1 {, [public|private] ClasseBaseI}1..n déclaration de classe

Exemples :

Déclaration Commentaire
class Cercle : public ObjetGraphique Héritage simple : Cercle dérive d'ObjetGraphique en mode public
class TexteGraphique : public ObjetGraphique,
public Chaine
Héritage multiple, (double en fait) : TexteGraphique dérive à la dois d'ObjetGraphique et de Chaine et ce, à chaque fois en public.
class Pile : private Vecteur Pile hérite uniquement de la classe Vecteur mais en private.

Tableau 5.1 : quelques exemples d'héritage

5.3 Le rôle du modificateur d’héritage et le modificateur d’accès protected

Un attribut protected n’est pas accessible à l’extérieur de la classe mais l’est aux classes dérivées. Ce modificateur est donc intermédiaire entre private et public. A l’instar de private, il respecte le principe d’encapsulation 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 d’hé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 l’hé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 d’un objet de la classe mère, il est logique qu’il 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 l’héritage en mode public.

5.3.1 De l’utilité de l’héritage en private

Nous avons vu que l’héritage en public et l’utilisation du modificateur d’accès protected traduit la notion classique d’héritage. Quel est donc l’utilité du modificateur d’héritage private qui cache à l’utilisateur 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 d’un exemple classique.

Soit la classe Pile. On peut l’implémenter à l’aide d’un tableau, d’une 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 d’une telle implémentation :

Avantages :

Inconvénients

Conclusion

De mon point de vue, la rigueur conceptuelle doit toujours primer sur l’effacité du code généré. Aussi, l’héritage, qu’il soit public ou private doit toujours rendre compte d’une relation de généralisation / spécialisation. Aussi, je n’utilise pas l’héritage en private pour traduire la relation “Est implémenté sous forme de”. Pour cela j’utilise de l’agrégation.

En effet, je pourrais écrire la classe Pile ainsi :

class Pile
{ 
  private: 
    ClasseDeStockage elements; 
}

Bien entendu, il n’est plus possible d’utiliser 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 n’est pas incohérent de dire :

“Une pile contient un objet de ClasseDeStockage pour stocker ces éléments”

En outre, et c’est un indicateur certain, la Librairie standard du C++ prône elle aussi l’utilisation de l’agrégation pour la relation “Est implémenté sous forme de”.

5.3.2 Faut-il déclarer private ou protected les attributs dans une classe ?

Rappelons tout d’abord qu’il est inconcevable de déclarer en public un attribut pour respecter le principe d’encapsulation des objets. Il nous reste donc la possibilité de les déclare protected ou private.

D’aucuns vous diront qu’il 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 l’hé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 d’intégrité. Par exemple, considérons une classe Hélicoptère avec 2 attributs qui sont l’angle d’incidence (l’inclinaison en avant ou en arrière de l’hélicoptère) et sa vitesse. La vitesse est calculée en fonction de divers paramètres, dont, en particulier l’incidence.

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 l’attribut était en protected, alors, la classe dérivée aura visibilité directe dessus, ce qui lui permettra de modifier la valeur de l’attribut incidence sans passer par la méthode modifierIncidence ce qui ne garantit plus l’intégrité de la vitesse.

Cet exemple parmi tant d’autres me conduit à m’interroger sur le bien fondé de placer tous les attributs en protected. Aussi, je me suis érigé une règle qui m’est 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 d’accès en lecture / écriture garantissant l'intégrité de l'objet lorsque cela s’avère nécessaire.

En outre, laisser des attributs en private permet de faire de faire de l’héritage sélectif.

5.4 Construction et destruction des objets des classes dérivées

La règle est simple :

Le constructeur d’une 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 :

  1. Vous spécifiez vous même l'appel au constructeur de la classe mère
  2. Vous ne le spécifiez pas, et il y a appel automatique du constructeur par défaut de la classe mère. Si celui-ci n'existe pas, le compilateur envoie un message d'erreur

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 :

Ordre des appels de constructeurs
TexteGraphique dérivant en premier d'ObjetGraphique puis de Chaine, il est naturel que le constructeur d'ObjetGraphique soit appelé en premier, suivi de celui de Chaine
Paramètres de construction
Nous considérons ici que le paramètre epaisseur n'est pas significatif pour un texte graphique. Ainsi, comme il n'est pas passé au constructeur de TexteGraphique, nous lui donnons une valeur « par défaut » dans celui d'ObjetGraphique. En revanche, nous ajoutons 2 paramètres spécifiques à l'affichage d'un texte graphique : le nom de la police et la taille des caractères.
Le code qui les utilise est ici purement démonstratif car ne correspondant à aucun système d'affichage réel. La seule partie intéressante réside dans la classe d'allocation de l'attribut laFonte. En effet, celui-ci étant dynamique, il est impératif de le détruire dans le constructeur de TexteGraphique
Destructeur
Le destructeur ~TexteGraphique est, bien entendu, chargé de détruire l'attribut dynamique laFonte. Toutefois, et cela ne se voit pas, il appelle également et automatiquement les destructeurs des classes ObjetGraphique et Chaine.
Si le destructeur d'ObjetGraphique est réduit à la portion congrue (la classe ne fait pas d'allocation dynamique et ne contient que des types élémentaires ainsi qu'un objet « simple » de la classe Point) il n'en est pas de même pour la classe Chaine qui, elle, fournit un destructeur chargé de désallouer le tableau de caractères au sein duquel elle stocke les éléments de la chaine de caractères
Notons que si les constructeurs des classes mères sont appelés au tout début du constructeur de la classe fille, le chaînage s'effectue dans l'autre sens pour les destructeurs : le code de la classe fille est exécuté avant l'appel aux classes mères.

5.5 Polymorphisme et classes abstraites

5.5.1 Polymorphisme : mise en place

Par défaut, en C++, une méthode n’est pas polymorphe (ce qui, à mon avis, va à l’encontre 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 :

virtual typeRetour identificateur(arguments) = 0;

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

5.5.2 Polymorphisme : exploitation

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.

5.5.3 Les tables de méthodes virtuelles

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 :

Classes et tables de méthodes virtuelles

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 :

o->tvm_[1]();

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.

5.5.4 Réutilisation du code de la classe mère :

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 :

  1. Une séquence d’instructions nommée « PRE » à loger, dans la mesure du possible, dans une méthode séparée
  2. Le code de la classe A
  3. Une séquence d’instructions nommée « POST » à loger, dans la mesure du possible, dans une méthode séparée

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 l’implé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.

5.6 Les difficultés liées à l'héritage multiple

L'héritage multiple pose de sérieux problèmes :

  1. La gestion des membres homonymes hérités des classes parentes
  2. La gestion de l'héritage à répétition

5.6.1 La gestion des homonymes dans l’héritage multiple

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 l’origine 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.

5.6.2 L'héritage virtuel pour régler les problèmes de l'héritage à répétition

5.6.2.1 Motivation

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 

5.6.2.2 Mise en oeuvre

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 :

  1. Les attributs de A ne seront présents qu'en un seul exemplaire, et ce, directement depuis A
  2. Le constructeur de A est explicitement appelé dans celui de D assurant ainsi l'initialisation correcte des attributs hérités de A. Conséquemment, les appels au constructeur de A depuis ceux de B et C ne seront pas exécutés.

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.

5.6.2.3 Conséquences néfastes de l'héritage virtuel

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.

5.6.2.4 Quand devez vous imposer de l'héritage virtuel ?

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".

5.6.2.5 Expérience sur l'héritage virtuel

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 :

Compilation :
essvirt.cc:62: warning: direct base `A' inaccessible in `D' due to ambiguity
Exécution :
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 !