[21] Héritage — l'héritage approprié et la substituabilité
(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 [21]


[21.1] Dois-je cacher les fonctions membres qui étaient publiques dans ma classe de base ?

Ne jamais, ô grand jamais faire ceci. Jamais. Jamais!

L'intention de cacher (d'éliminer, de révoquer, ou de privatiser) des fonctions membres publiques héritées est une erreur de conception bien trop fréquente. Cela provient généralement d'une pensée un peu trop confuse.

(Note: cette FAQ est relative à l'héritage public; l'héritage privé et l'héritage protégé sont différents.)

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


[21.2] Derived* —> Base* fonctionne bien; pourquoi Derived** —> Base** ne fonctionne t'il pas?

C++ permet à un Derived* d'être converti en un Base*, car un objet Derived est une sorte d'objet Base. Cependant essayer de convertir un Derived** en un Base** est considéré comme une erreur. Bien que cette erreur ne sois pas flagrante, c'est loin d'être une bonne chose. Par exemple, on pourrait convertir un Car** en un Vehicle**, et similairement on pourrait convertir un NuclearSubmarine** en un Vehicle**, on pourrait affecter ces deux pointeurs et finir avec un Car* qui pointe sur un NuclearSubmarine:

    class Vehicle {
    public:
      virtual ~Vehicle() { }
      virtual void startEngine() = 0;
    };

    class Car : public Vehicle {
    public:
      virtual void startEngine();
      virtual void openGasCap();
    };

    class NuclearSubmarine : public Vehicle {
    public:
      virtual void startEngine();
      virtual void fireNuclearMissle();
    };

    int main()
    {
      Car   car;
      Car*  carPtr = &car;
      Car** carPtrPtr = &carPtr;
      Vehicle** vehiclePtrPtr = carPtrPtr;  // Ceci est une erreur en C++
      NuclearSubmarine  sub;
      NuclearSubmarine* subPtr = ⊂
      *vehiclePtrPtr = subPtr;      // Cette ligne aurait permis à carPtr de pointer sur sub !
      carPtr->openGasCap();  // Cela aurait pu déclencher le tir d'un missile nucléaire via fireNuclearMissle()!
    }

En d'autres termes, si il était légal de convertir un Derived** en un Base**, le Base** aurait pu être déréférencé et le Base* aurait pu être pointé sur un objet d'une classe dérivée différente qui aurait pu causer de sérieux problèmes à la sécurité nationale (qui sait ce qu'il serait advenu si vous aviez voulu ouvrir le bouchon de réservoir via la fonction membre openGasCap() sur ce que vous pensiez être une voiture alors qu'il s'agissait en réalité d'un sous-marin nucléaire!! Essayez le code ci dessus -- sur la plupart des compilateurs cela appellera NuclearSubmarine::fireNuclearMissile()!

(Note: cette FAQ est relative à l'héritage public; l'héritage privé et l'héritage protégé sont différents.)

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


[21.3] un parking-de-voiture est-il une sorte-de parking-de-véhicule?

Non.

Je sais que cela peut paraître étrange, mais c'est la réalité. Vous pouvez voir cela comme la conséquence directe de la question précédente ou vous pouvez le voir de la façon suivante: Si la relation sorte-de était valide, alors cela signifierait que l'on aurait pu faire pointer le pointeur de parking-lot-of-Vehicle sur un parking-lot-of-Car. Mais parking-lot-of-Vehicle défini la fonction membre addNewVehicleToParkingLot(Vehicle&) qui permet d'ajouter n'importe quel objet véhicule au parking. Cela permet donc de parquer un NuclearSubmarine dans un parking-lot-of-Car. Il peut être surprenant de sortir du parking ce que l'on pensait être une voiture alors qu'il s'agit en réalité d'un sous-marin nucléaire.

Encore une autre manière d'énoncer cette vérité vraie: un contenant de truc n'est pas une sorte de contenant de n'importe quoi, même si un truc est une sorte de n'importe quoi. Vous pouvez déglutir de nouveau; c'est la vérité.

On ne vous demande pas d'aimer, mais vous devez l'accepter.

Un dernier exemple utilisé dans nos formations OO/C++: "un sac de pommes n'est pas un sac de fruits" Si un sac de pommes peut être assimilé à un sac de fruits alors quelqu'un aurait trés bien pu glisser une banane dans ce sac alors qu'il n'était censé contenir que des pommes!

(Note: cette FAQ est relative à l'héritage public; l'héritage privé et l'héritage protégé sont différents.)

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


[21.4] Est ce qu'un tableau de Derived est une-sorte-de (kind-of) tableau de Base?

[Recently added return type to main() (on 10/99).]

Non.

C'est le corollaire de la FAQ précédente. Malheureusement, celui là peut vous amener pas mal d'ennui. Considérez ceci:

    class Base {
    public:
      virtual void f();             // 1
    };

    class Derived : public Base {
    public:
      // ...
    private:
      int i_;                       // 2
    };

    void userCode(Base* arrayOfBase)
    {
      arrayOfBase[1].f();           // 3
    }

    int main()
    {
      Derived arrayOfDerived[10];   // 4
      userCode(arrayOfDerived);     // 5
    }

Le compilateur pense que le typage est parfaitement correct. La ligne 5 convertit un Derived* en un Base*. En réalité cela est diaboliquement insidieux: sachant que Derived a une taille supérieure à Base, l'arithmétique du pointeur effectué ligne 3 est incorrect: le compilateur utilise (sizeof(Base)) quand il calcule l'adresse de arrayOfBase[1], alors qu'il s'agit d'un Derived, ce qui signifie que l'adresse calculée (et par la meme l'appel de la fonction f() ) n'est meme pas sur le debut d'un objet ! Elle est en plein milieu de l'objet Derived. En supposant que votre compilateur utilise l'approche classique des fonctions virtuelles , cela va réinterpréter le i_ du premier Derived comme si il pointait sur la table virtuelle, va suivre l'adresse désigné par le pointeur (ce qui signifie que l'on va aller chercher une valeur dans un emplacement mémoire de manière aléatoire) et récupérer les premiers mots de cet emplacement mémoire et l'interpreter comme si il s'agissait de l'adresse d'une fonction membre C++, la chargera en mémoire (emplacement mémoire aléatoire) et commencera à interpréter les instructions machines de cet emplacement. La probabilité de plantage est plus qu'élevée.

La racine du problème est que le C++ ne peut pas distinguer un pointeur-sur-truc d'un pointeur-sur-tableau-de-trucs. Naturellement le C++ hérite cette fonctionnalité du C.

NOTE: Si nous avions utilisé une classe array-like (e.g, vector<Derived> de la STL ) plutôt qu'un tableau brut, le problème aurait été dénoncé lors de la compilation plutôt que lors d'un résultat d'une exécution désastreuse.

(Note: cette FAQ est relative à l'héritage public; l'héritage privé et l'héritage protégé sont différents.)

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


[21.5] Est-ce mauvais qu'un tableau de Derived ne soit pas une-sorte-de tableau de Base ?

Oui, les tableaux sont un fléau. (je plaisante à moitié).

Sérieusement, les tableaux sont trés proches des pointeurs et les pointeurs sont notoirement difficiles à gérer. Mais si vous avez parfaitement saisi en quoi les FAQs ci-avant sont un problème d'un point de vue conceptuel (e.g. si vous savez pourquoi un container de Truc n'est pas une sorte-de container de n'importe quoi), et que vous pensez que ceux qui vont maintenir votre code ont également parfaitement saisi ces concepts OO, alors vous pouvez utiliser les tableaux. Mais si vous êtes comme la majorité des gens, vous devriez utiliser un container sous forme de template tel que vector<T> de la  STL plutôt que des tableaux.

(Note: cette FAQ est relative à l'héritage public; l'héritage privé et l'héritage protégé sont différents.)

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


[21.6] Un cercle est t'il une sorte-d'ellipse?

Non, même si une Ellipseest susceptible de changer sa taille asymétriquement.

Par exemple, supposons que Ellipse a une méthode membre setSize(x,y), et supposons que cette fonction membre implique que width() de Ellipse sera x, et que height() sera y. Alors dans ce cas, Circle ne peut être une sorte-de Ellipse. Simplement, si Ellipse peut faire quelque chose que Circle ne peut pas, alors Circle n'est pas une sorte-de Ellipse.

Cela laisse deux relations potentielles (valides) entre Circle et Ellipse:

Dans le premier cas Ellipse peut être dérivé de AsymmetricShape, et setSize(x,y)peut être introduit dans AsymmetricShape. Cependant Circlepeut être dérivé de SymmetricShapequi a une fonction membre setSize(size).

Dans le second cas, la classe Oval peut avoir uniquement setSize(size) qui défini à la fois width() et height(). Ellipse et Circle peuvent tout les deux hérités de Oval. Ellipse —mais pas Circle— peut définir l'opération setSize(x,y) (mais attention aux méthodes cachées si la même fonction membre nommée setSize() est utilisée pour les deux opérations).

(Note: cette FAQ est relative à l'héritage public; l'héritage privé et l'héritage protégé sont différents.)

(Note: setSize(x,y) n'est pas sacrée. Suivant votre but, il peut être correct de ne pas permettre aux utilisateurs de changer les dimensions de Ellipse, dans ce cas il peut etre conceptuellement correct de ne pas avoir une méthode setSize(x,y) dans Ellipse. Cependant cette série de FAQs discute de la technique à adopter quand vous souhaitez créer une classe dérivée d'une classe pré-existante qui contient une méthode "inacceptable". Bien sûr, la situation idéale est de découvrir ce problème quand la classe de base n'existe pas encore. Mais la vie n'est pas toujours parfaite...)

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


[21.7] Y-a-t'il d'autres options concernant le dilemne "un cercle est/n'est pas une sorte-d'ellipse" ?

Si vous souhaitez que toutes les Ellipses puissent être dimensionnées asymétriquement et que vous souhaitez que Circlene puisse pas être dimensionné asymétriquement, clairement, vous devez choisir l'un des deux. Donc vous avez soit à éliminer Ellipse::setSize(x,y), soit à éliminer la relation d'héritage entre Circle et Ellipse, où vous devez admettre que vos cercles Circlene sont pas nécessairement circulaires.

Voici les deux plus grands pièges dans lequels les programmeurs C++/OO tombent régulièrement. Ils essayent d'utiliser de bidouiller du code pour réparer une conception hasardeuse (ils redéfinissent  Circle::setSize(x,y)qui lance une exception, appellent abort(), choisissent la moyenne des deux paramètres ou en font une fonction vide). Malheureusement, chacune de ces bidouilles surprendra l'utilisateur car il s'attendra à trouver width() == x and height() == y. La seule chose que vous ne devez pas faire est de surprendre vos utilisateurs.

Si il est important pour vous de garder la relation d'héritage: "Circle est une sorte-de Ellipse", vous pouvez affaiblir la promesse faite par setSize(x,y) de Ellipse. E.g., vous pouvez changer cette promesse en "Cette fonction membre peut mettre width() à x et/ou peut mettre height() à y, ou peut ne rien faire". Malheureusement cela dilue fortement la promesse initiale car l'utilisateur ne peut pas se baser sur un comportement significatif. La hiérarchie entière commence donc à devenir un peu chancelante. (Il est difficile de convaincre quelqu'un d'utiliser un objet si vous avez à hausser vos épaules quand on vous demande ce qu'il fait réellement)

(Note: cette FAQ est relative à l'héritage public; l'héritage privé et l'héritage protégé sont différents.)

(Note: setSize(x,y) n'est pas sacrée. Suivant votre but, il peut être correct de ne pas permettre aux utilisateurs de changer les dimensions de Ellipse, dans ce cas il peut etre conceptuellement correct de ne pas avoir une méthode setSize(x,y) dans Ellipse. Cependant cette série de FAQs discute de la technique à adopter quand vous souhaitez créer une classe dérivée d'une classe pré-existante qui contient une méthode "inacceptable". Bien sûr, la situation idéale est de découvrir ce problème quand la classe de base n'existe pas encore. Mais la vie n'est pas toujours parfaite...)

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


[21.8] Mais, j'ai une thèse en Mathématique, et je suis sûr qu'un cercle est une sorte d'ellipse! Cela signifie t'il que Marshall Cline (N.D.T.: l'auteur de cette FAQ) est idiot? Ou que le C++ est idiot? Ou que l'OO est idiot?

En fait, cela ne signifie rien de cela. La triste réalité, c'est que cela signifie que votre intuition est mauvaise.

Ecoutez, j'ai reçu et répondu à des douzaines de courrier électronique passionnés par ce sujet. Je l'ai enseigné des centaines de fois à des milliers de professionnels du logiciel un peu partout. Je sais que cela est contre votre intuition. Mais vous pouvez me croire, votre intuition est mauvaise.

Le réel problème, c'est que votre notion intuitive de "sorte-de" ne correspond pas à la notion OO d'héritage(techniquement appelé le sous-typage). L'essentiel, c'est que les objets de la classe dérivée doivent être substituable aux objets de la classe de base. Dans le cas cercle/ellipse, la fonction membre setSize(x,y) viole cette substituabilité.

Vous avez trois possibilités: [1] vous supprimez la fonction membre setSize(x,y) de Ellipse  (et donc vous cassez le code existant qui appelle la fonction membre setSize(x,y)), [2] vous permettez à Circle d'avoir une hauteur et une largeur différente (hum..un cercle asymétrique...), ou [3] vous abandonnez la relation d'héritage. Désolé, mais il n'y a simplement pas d'autres choix. Notez que certaines personnes mentionnent également la possibilité de dériver Circle et Ellipse d'une classe de base commune, mais il s'agit juste d'une variante de l'option [3].

Une autre façon de dire cela, c'est que vous avez, soit à rendre votre classe de base fonctionnellement moins riche (cela revient à modifier Ellipse de manière à ce que l'on ne puisse pas mettre sa largeur différente de sa hauteur), ou rendre la classe dérivée fonctionnellement plus riche (dans ce cas rendre possible le dimensionnement de Circle de manière symétrique et, hum..asymétrique). Quand aucun de ces deux possibilité n'est satisfaisante ( comme dans le cas cercle/ellipse), générallement on supprime la relation d'héritage. Si la relation d'héritage doit exister, alors vous aurez certainement à supprimer les fonctions membres mutantes (setHeight(y), setWidth(x), and setSize(x,y)) de la classe de base.

(Note: cette FAQ est relative à l'héritage public; l'héritage privé et l'héritage protégé sont différents.)

(Note: setSize(x,y) n'est pas sacrée. Suivant votre but, il peut être correct de ne pas permettre aux utilisateurs de changer les dimensions de Ellipse, dans ce cas il peut etre conceptuellement correct de ne pas avoir une méthode setSize(x,y) dans Ellipse. Cependant cette série de FAQs discute de la technique à adopter quand vous souhaitez créer une classe dérivée d'une classe pré-existante qui contient une méthode "inacceptable". Bien sûr, la situation idéale est de découvrir ce problème quand la classe de base n'existe pas encore. Mais la vie n'est pas toujours parfaite...)

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


[21.9] Mais mon problème n'a rien à voir avec les cercles et les ellipses, alors qu'est ce que ce stupide exemple m'apporte ?

Ahhh, voilà le hic!. Vous pensez que l'exemple cercle/ellipse est juste un stupide exemple. Mais en réalité, votreproblème est un isomorphisme de cet exemple.

Je me moque de savoir votre problème d'héritage, mais tout(oui tout) les mauvais héritages se réduisent à l'exemple du cercle-qui-n'est-pas-une-sorte-d'ellipse.

Voici pourquoi: Les mauvais héritages ont tout le temps une classe de base avec une action bien spécifique(la plupart du temps une fonction  membre ou deux, parfois une promesse effectuée par la combinaison de fonctions membres) qu'une classe dérivée ne peut satisfaire. Vous avez soit à alléger votre classe de base d'un point de vue fonctionnel,  augmenter les fonctionnalités de votre classe dérivée ou éliminer la relation d'héritage. J'ai vu de nombreux exemple de propositions d'héritages, et croyez moi, ils se réduisent tous à l'exemple cercle/ellipse.

Donc, si vous comprenez vraiment le problème cercle/ellipse, vous serez capable de reconnaître des problèmes d'héritages un peu partout. Si vous ne comprenez pas ce qu'il se trame sous le problème cercle/ellipse, il y a de fortes chances que vous allez faire de trés graves et de très coûteuses erreurs.

Malheureusement, c'est la vérité.

(Note: cette FAQ est relative à l'héritage public; l'héritage privé et l'héritage protégé sont différents.)

[ 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