web-dev-qa-db-fra.com

Pourquoi devrais-je éviter les héritages multiples en C ++?

Est-ce un bon concept d'utiliser l'héritage multiple ou puis-je faire d'autres choses à la place?

158
Hai

Héritage multiple (abrégé MI) sent, ce qui signifie que d'habitude, cela a été fait pour de mauvaises raisons, et cela va se retourner contre le responsable.

Sommaire

  1. Considérer la composition des caractéristiques, au lieu de l'héritage
  2. Méfiez-vous du diamant de l'effroi
  3. Considérer l'héritage de plusieurs interfaces au lieu d'objets
  4. Parfois, l'héritage multiple est la bonne chose. Si c'est le cas, utilisez-le.
  5. Soyez prêt à défendre votre architecture multi-héritée dans les revues de code.

1. Peut-être la composition?

Ceci est vrai pour l'héritage, et donc encore plus vrai pour l'héritage multiple.

Votre objet a-t-il vraiment besoin d'hériter d'un autre? Un Car n'a pas besoin d'hériter d'un Engine pour fonctionner, ni d'un Wheel. Un Car a un Engine et quatre Wheel.

Si vous utilisez l'héritage multiple pour résoudre ces problèmes au lieu de la composition, vous avez commis une erreur.

2. Le diamant de l'effroi

Généralement, vous avez une classe A, puis B et C héritent de A. Et (ne me demandez pas pourquoi), quelqu'un décide alors que D doit hériter à la fois de B et de C.

J'ai rencontré ce genre de problème deux fois en 8 ans, et il est amusant de le voir à cause de:

  1. Quelle erreur a été commise au début (dans les deux cas, D n'aurait pas dû hériter des deux B et C), car il s'agissait d'une mauvaise architecture (en fait , C n'aurait pas du tout existé ...)
  2. Combien les responsables payaient-ils pour cela, car en C++, la classe parente A était présente deux fois dans sa classe de petits-enfants D, et mettait ainsi à jour un champ parent A::field voulait dire soit le mettre à jour deux fois (via B::field et C::field), ou si quelque chose va silencieusement mal et se plante, plus tard (nouveau pointeur dans B::field, et supprimez C::field...)

Utiliser le mot-clé virtual en C++ pour qualifier l'héritage évite la double mise en page décrite ci-dessus si ce n'est pas ce que vous voulez, mais de toute façon, d'après mon expérience, vous faites probablement quelque chose de mal.

Dans la hiérarchie d'objets, vous devez essayer de conserver la hiérarchie sous forme d'arborescence (un nœud a UN parent) et non sous forme de graphique.

En savoir plus sur le diamant (edit 2017-05-03)

Le vrai problème avec le Diamond of Dread en C++ (en supposant que la conception soit saine, faites réviser votre code!), est-ce vous devez faire un choix:

  • Est-il souhaitable que la classe A existe deux fois dans votre mise en page et que signifie-t-elle? Si oui, alors certainement en hériter deux fois.
  • s'il ne doit exister qu'une seule fois, alors en hériter virtuellement.

Ce choix est inhérent au problème et, en C++, contrairement aux autres langages, vous pouvez le faire sans que le dogme ne force votre conception au niveau des langages.

Mais comme tous les pouvoirs, ce pouvoir s'accompagne de responsabilités: faites réviser votre conception.

3. Interfaces

L'héritage multiple de zéro ou d'une classe concrète, et de zéro ou plusieurs interfaces est généralement acceptable, car vous ne rencontrerez pas le diamant de l'effroi décrit ci-dessus. En fait, c'est comme cela que les choses se font en Java.

Habituellement, ce que vous voulez dire quand C hérite de A et B est que les utilisateurs peuvent utiliser C comme s'il s'agissait de A, et/ou comme c'était un B.

En C++, une interface est une classe abstraite qui a:

  1. toute sa méthode déclarée pure virtuelle (suffixé par = 0)(Supprimé le 2017-05-03)
  2. pas de variables membres

L'héritage multiple de zéro en un objet réel et de zéro ou plusieurs interfaces n'est pas considéré comme "malodorant" (du moins, pas autant).

En savoir plus sur l'interface abstraite C++ (edit 2017-05-03)

Tout d’abord, le modèle NVI peut être utilisé pour produire une interface, car le vrai critère est de ne pas avoir d'état (c.-à-d. aucune variable membre, sauf this). Le but de votre interface abstraite est de publier un contrat ("vous pouvez m'appeler ainsi et ainsi"), rien de plus, rien de moins. La limitation de l'utilisation d'une méthode virtuelle abstraite devrait être un choix de conception et non une obligation.

Deuxièmement, en C++, il est logique d'hériter virtuellement d'interfaces abstraites (même avec le coût/indirection supplémentaire). Si vous ne le faites pas et que l'héritage d'interface apparaît plusieurs fois dans votre hiérarchie, vous aurez des ambiguïtés.

Troisièmement, l'orientation des objets est excellente, mais ce n'est pas le cas. La seule vérité là-basTM en C++. Utilisez les bons outils et souvenez-vous toujours que vous avez d'autres paradigmes en C++ offrant différents types de solutions.

4. Avez-vous vraiment besoin de l'héritage multiple?

Parfois oui.

Habituellement, votre classe C hérite de A et B, et A et B sont deux objets non liés (c'est-à-dire qui ne sont pas même hiérarchie, rien en commun, concepts différents, etc.).

Par exemple, vous pourriez avoir un système de Nodes avec des coordonnées X, Y, Z, capable de faire beaucoup de calculs géométriques (peut-être un point, une partie d'objets géométriques) et chaque Node est un agent automatisé, capable de communiquer avec d'autres agents.

Peut-être avez-vous déjà accès à deux bibliothèques, chacune avec son propre espace de noms (une autre raison d'utiliser des espaces de noms ... Mais vous utilisez des espaces de noms, n'est-ce pas?), L'une étant geo et l'autre ai

Donc, vous avez votre propre own::Node dérivent tous deux de ai::Agent et geo::Point.

C'est le moment où vous devriez vous demander si vous ne devriez pas utiliser la composition à la place. Si own::Node _ est vraiment à la fois un ai::Agent et un geo::Point, alors la composition ne fera pas.

Ensuite, vous aurez besoin de plusieurs héritages, ayant votre own::Node communiquez avec les autres agents en fonction de leur position dans un espace 3D.

(Vous remarquerez que ai::Agent et geo::Point sont complètement, totalement, totalement inexploités ... Cela réduit considérablement le risque d'héritage multiple)

Autres cas (modifier 2017-05-03)

Il y a d'autres cas:

  • utiliser l'héritage (heureusement privé) comme détail d'implémentation
  • certains idiomes C++, comme les politiques, peuvent utiliser l'héritage multiple (lorsque chaque partie doit communiquer avec les autres via this)
  • l'héritage virtuel de std :: exception ( L'héritage virtuel est-il nécessaire pour les exceptions? )
  • etc.

Parfois, vous pouvez utiliser la composition, et parfois, MI est préférable. Le fait est que vous avez le choix. Faites-le de manière responsable (et faites réviser votre code).

5. Alors, devrais-je faire l'héritage multiple?

La plupart du temps, selon mon expérience, non. MI n'est pas le bon outil, même si cela semble fonctionner, car il peut être utilisé par des paresseux pour empiler des entités sans en prendre conscience des conséquences (comme créer un Car à la fois un Engine et un Wheel).

Mais parfois, oui. Et à ce moment-là, rien ne fonctionnera mieux que MI.

Mais comme MI sent mauvais, soyez prêt à défendre votre architecture dans les revues de code (et la défendre est une bonne chose, car si vous n'êtes pas en mesure de le défendre, vous ne devriez pas le faire).

252
paercebal

D'après un entretien avec Bjarne Stroustrup :

Les gens disent tout à fait correctement que vous n’avez pas besoin d’un héritage multiple, car tout ce que vous pouvez faire avec un héritage multiple vous pouvez également le faire avec un héritage unique. Vous utilisez simplement le truc de délégation que j'ai mentionné. De plus, vous n'avez besoin d'aucun héritage, car tout ce que vous faites avec un héritage unique peut également être évité sans héritage en transmettant via une classe. En fait, vous n’avez pas besoin de classes non plus, car vous pouvez tout faire avec des pointeurs et des structures de données. Mais pourquoi tu veux faire ça? Quand est-il commode d'utiliser les installations linguistiques? Quand préféreriez-vous une solution de contournement? J'ai vu des cas où l'héritage multiple est utile, et j'ai même vu des cas où l'héritage multiple assez compliqué est utile. En général, je préfère utiliser les fonctionnalités offertes par la langue que pour résoudre des problèmes.

140
Nemanja Trifunovic

Il n'y a aucune raison de l'éviter et cela peut être très utile dans certaines situations. Vous devez cependant être conscient des problèmes potentiels.

Le plus gros étant le diamant de la mort:

class GrandParent;
class Parent1 : public GrandParent;
class Parent2 : public GrandParent;
class Child : public Parent1, public Parent2;

Vous avez maintenant deux "copies" de GrandParent dans Child.

C++ a cependant pensé à cela et vous permet de créer un héritage virtuel pour résoudre les problèmes.

class GrandParent;
class Parent1 : public virtual GrandParent;
class Parent2 : public virtual GrandParent;
class Child : public Parent1, public Parent2;

Vérifiez toujours votre conception, assurez-vous que vous n'utilisez pas l'héritage pour économiser sur la réutilisation des données. Si vous pouvez représenter la même chose avec la composition (et généralement vous le pouvez), c'est une approche bien meilleure.

38
billybob

Voir w: Héritage multiple .

L'héritage multiple a fait l'objet de critiques et n'est donc pas implémenté dans de nombreuses langues. Les critiques comprennent:

  • Complexité accrue
  • L'ambiguïté sémantique est souvent résumée comme problème du diamant .
  • Ne pas pouvoir explicitement hériter de plusieurs fois d'une même classe
  • Ordre d'héritage changeant la sémantique de la classe.

L'héritage multiple dans les langages avec des constructeurs de style C++/Java exacerbe le problème de l'héritage des constructeurs et de l'enchaînement des constructeurs, créant ainsi des problèmes de maintenance et d'extensibilité dans ces langages. Les objets en relations d'héritage avec des méthodes de construction très variables sont difficiles à implémenter dans le paradigme de l'enchaînement des constructeurs.

Manière moderne de résoudre ceci en utilisant une interface (classe abstraite pure) comme COM et Java.

Je peux faire d'autres choses à la place de cela?

Oui, vous pouvez. Je vais voler de GoF .

  • Programmer une interface, pas une implémentation
  • Préfère la composition à l'héritage
11
Eugene Yokota

L'héritage public est une relation IS-A, et parfois une classe sera un type de plusieurs classes différentes, et il est parfois important de refléter cela.

Les "mixins" sont aussi parfois utiles. Ce sont généralement de petites classes, n'héritant généralement de rien, offrant des fonctionnalités utiles.

Tant que la hiérarchie des héritages est assez superficielle (comme il se doit presque toujours) et qu'elle est bien gérée, il est peu probable que vous obteniez l'héritage redouté de Diamond. Le losange n'est pas un problème avec toutes les langues qui utilisent l'héritage multiple, mais le traitement de C++ par celui-ci est souvent maladroit et parfois déroutant.

Bien que je me sois retrouvé dans des cas où l'héritage multiple est très pratique, ils sont en fait assez rares. C'est probablement parce que je préfère utiliser d'autres méthodes de conception lorsque je n'ai pas vraiment besoin d'héritage multiple. Je préfère éviter les constructions de langage qui prêtent à confusion, et il est facile de construire des cas d'héritage dans lesquels vous devez vraiment bien lire le manuel pour comprendre ce qui se passe.

8
David Thornley

Vous ne devez pas "éviter" l'héritage multiple, mais vous devez être conscient des problèmes pouvant survenir tels que le "problème du diamant" ( http://en.wikipedia.org/wiki/Diamond_problem ) et traiter le pouvoir qui vous est donné avec soin, comme vous devriez le faire avec tous les pouvoirs.

6
jwpfox

Nous utilisons Eiffel. Nous avons un excellent MI. Pas de soucis. Pas d'issues. Facilement géré. Il y a des moments pour ne pas utiliser MI. Cependant, cela est plus utile que les gens ne le réalisent, car ils sont: A) dans un langage dangereux qui ne le gère pas bien - OU - B) satisfaits de la façon dont ils ont travaillé avec MI pendant des années et des années - OU - C) pour d'autres raisons ( trop nombreux pour être énumérés, j'en suis certain - voir les réponses ci-dessus).

Pour nous, en utilisant Eiffel, MI est aussi naturel que toute autre chose et constitue un autre outil précieux de la boîte à outils. Franchement, nous sommes assez indifférents au fait que personne n’utilise Eiffel. Pas de soucis. Nous sommes satisfaits de ce que nous avons et vous invitons à jeter un coup d’œil.

Pendant que vous regardez: prenez des notes particulières sur la sécurité du vide et l'éradication du déréférencement du pointeur Null. Pendant que nous dansons tous autour de MI, vos pointeurs se perdent! :-)

2
Larry

Vous devez l’utiliser avec précaution, dans certains cas, comme le problème de diamant , lorsque les choses peuvent devenir compliquées.

alt text
(source: learncpp.com )

2
CMS

Chaque langage de programmation a un traitement légèrement différent de la programmation orientée objet avec le pour et le contre. La version de C++ met clairement l'accent sur les performances et présente l'inconvénient qu'il est extrêmement facile d'écrire du code non valide, ce qui est vrai de l'héritage multiple. En conséquence, il existe une tendance à éloigner les programmeurs de cette fonctionnalité.

D'autres personnes ont abordé la question de savoir pourquoi l'héritage multiple n'est pas bon. Mais nous avons vu un certain nombre de commentaires qui impliquent plus ou moins que la raison pour éviter cela est parce que ce n'est pas sûr. Eh bien oui et non.

Comme c'est souvent le cas en C++, si vous suivez une directive de base, vous pouvez l'utiliser en toute sécurité sans avoir à "regarder par-dessus votre épaule" en permanence. L'idée principale est de distinguer un type spécial de définition de classe appelé "mix-in"; class est une combinaison si toutes ses fonctions membres sont virtuelles (ou virtuelles virtuelles). Ensuite, vous êtes autorisé à hériter d'une classe principale unique et d'autant de "mix-ins" que vous le souhaitez - mais vous devez hériter de mixins avec le mot clé "virtual". par exemple.

class CounterMixin {
    int count;
public:
    CounterMixin() : count( 0 ) {}
    virtual ~CounterMixin() {}
    virtual void increment() { count += 1; }
    virtual int getCount() { return count; }
};

class Foo : public Bar, virtual public CounterMixin { ..... };

Ma suggestion est que si vous envisagez d'utiliser une classe comme classe mixte, vous devez également adopter une convention de dénomination afin de permettre à toute personne lisant le code de voir facilement ce qui se passe et de vérifier que vous respectez les règles de la directive de base. . Et vous constaterez que cela fonctionne beaucoup mieux si vos mix-ins ont aussi des constructeurs par défaut, juste à cause du fonctionnement des classes de base virtuelles. Et n'oubliez pas de rendre tous les destructeurs virtuels aussi.

Notez que mon utilisation du mot "mix-in" ici n'est pas la même que la classe de modèle paramétrée (voir ce lien pour une bonne explication) mais je pense que c'est une utilisation juste de la terminologie .

Maintenant, je ne veux pas donner l’impression que c’est le seul moyen d’utiliser le patrimoine multiple en toute sécurité. C'est juste un moyen assez facile à vérifier.

2
sfkleach

Au risque de devenir un peu abstrait, je trouve éclairant de penser à l’héritage dans le cadre de la théorie des catégories.

Si nous pensons à toutes nos classes et aux flèches entre elles qui dénotent des relations d’héritage, alors quelque chose comme ceci

A --> B

signifie que class B dérive de class A. Notez que, étant donné

A --> B, B --> C

nous disons que C dérive de B qui dérive de A, donc C est également dit dériver de A, donc

A --> C

De plus, nous disons que pour chaque classe A que trivialement A dérive de A, notre modèle d'héritage complète donc la définition d'une catégorie. En langage plus traditionnel, nous avons une catégorie Class avec des objets toutes classes et morphismes les relations d'héritage.

C'est un peu compliqué, mais avec cela, jetons un coup d'œil à notre Diamond of Doom:

C --> D
^     ^
|     |
A --> B

C'est un diagramme qui semble louche, mais ça ira. Donc, D hérite de tous les A, B et C. De plus, pour se rapprocher de la question de OP, D hérite également de toute superclasse de A. Nous pouvons dessiner un diagramme

C --> D --> R
^     ^
|     |
A --> B
^ 
|
Q

Maintenant, les problèmes associés au diamant de la mort sont ici quand C et B partagent des noms de propriétés/méthodes et que les choses deviennent ambiguës; Cependant, si nous déplaçons tout comportement partagé dans A, alors l'ambiguïté disparaîtra.

En termes catégoriques, nous voulons que A, B et C soient tels que si B et C héritent de Q alors A peut être réécrit comme une sous-classe de Q. Cela rend A quelque chose appelé un pushout .

Il y a aussi une construction symétrique sur D appelée un retrait . C’est essentiellement la classe utile la plus générale que vous puissiez construire et qui hérite à la fois de B et de C. Autrement dit, si vous avez une autre classe R multiplier héritant de B et C, alors D est une classe où R peut être réécrit en tant que sous-classe de D.

En vous assurant que vos pointes du diamant sont bien en retrait et qu’elles sont en retrait, nous sommes un bon moyen de traiter de manière générique les problèmes de conflit de noms ou d’entretien qui pourraient survenir autrement.

Remarque Paercebal 's réponse a inspiré ceci car ses remontrances sont sous-entendues par le modèle ci-dessus étant donné que nous travaillons dans la catégorie complète de toutes les classes possibles.

Je voulais généraliser son argument à quelque chose qui montre à quel point la complexité des relations d'héritage multiples peut être à la fois puissante et non problématique.

TL; DR Pensez aux relations d'héritage dans votre programme comme formant une catégorie. Ensuite, vous pouvez éviter les problèmes Diamond of Doom en créant des extensions de classes héritées de plusieurs générations et de manière symétrique, en créant une classe parente commune qui constitue un retrait.

2
Ncat

sages et abus d'héritage.

L'article explique très bien l'héritage et ses dangers.

1
csexton

Le problème clé avec MI des objets concrets est que vous avez rarement un objet qui devrait légitimement "être un A et être un B", il est donc rarement la solution correcte pour des raisons logiques. Beaucoup plus souvent, vous avez un objet C qui obéit "C peut agir comme un A ou un B", ce que vous pouvez obtenir via l'héritage et la composition d'interface. Mais ne vous y trompez pas: l'héritage de plusieurs interfaces est encore MI, mais un sous-ensemble de celui-ci.

Pour C++ en particulier, la faiblesse principale de la fonctionnalité n’est pas l’existence réelle d’EXISTENCE de l'héritage multiple, mais certaines constructions qu'il autorise sont presque toujours mal formées. Par exemple, hériter de plusieurs copies du même objet comme:

class B : public A, public A {};

est malformé PAR DÉFINITION. Traduit en anglais c'est "B est un A et un A". Donc, même dans le langage humain, il y a une grande ambiguïté. Voulez-vous dire "B a 2 As" ou juste "B est un A" ?. Autoriser un tel code pathologique et, pire encore, en faire un exemple d'utilisation, ne facilitait pas le C++ lorsqu'il était question de conserver la fonctionnalité dans les langues suivantes.

1
Zack Yezek

Au-delà du modèle en losange, l'héritage multiple tend à rendre le modèle d'objet plus difficile à comprendre, ce qui augmente les coûts de maintenance.

La composition est intrinsèquement facile à comprendre, à comprendre et à expliquer. Écrire du code peut être fastidieux, mais bon IDE (cela fait quelques années que je travaille avec Visual Studio, mais certainement le Java Les IDE ont tous d'excellents raccourcis de composition, des outils d'automatisation qui devraient vous permettre de surmonter cet obstacle.

En outre, en termes de maintenance, le "problème de diamant" se pose également dans les instances d'héritage non littérales. Par exemple, si vous avez A et B et que votre classe C les étend toutes les deux, et A a une méthode 'makeJuice' qui fabrique du jus d'orange et que vous prolongez le jus d'orange avec une touche de citron vert: que se passe-t-il lorsque le concepteur ' B 'ajoute une méthode' makeJuice 'qui génère du courant électrique? 'A' et 'B' peuvent être des "parents" compatibles en ce moment, mais cela ne signifie pas qu'ils le seront toujours!

Globalement, la maxime consistant à éviter l’héritage, et en particulier l’héritage multiple, est valable. Comme pour toutes les maximes, il existe des exceptions, mais vous devez vous assurer qu'il y a un panneau lumineux au néon vert clignotant indiquant les exceptions que vous codez (et entraîner votre cerveau de sorte que chaque fois que vous voyez de tels arbres d'héritage, vous tracez votre propre néon vert clignotant. signe) et que vous vérifiez que tout a du sens de temps en temps.

1
Tom Dibble

cela prend 4/8 octets par classe impliquée. (Un ce pointeur par classe).

Cela ne sera peut-être jamais un problème, mais si vous avez un jour une structure de micro-données instanciée de milliards de dollars, ce le sera.

0
Ronny Brendel

Vous pouvez utiliser la composition de préférence à l'héritage.

Le sentiment général est que la composition est meilleure et qu’elle est très bien discutée.

0
Ali Afshar