Pourquoi devrais-je éviter l’inheritance multiple en C ++?

Est-ce un bon concept d’utiliser l’inheritance multiple ou puis-je faire d’autres choses à la place?

Related of "Pourquoi devrais-je éviter l’inheritance multiple en C ++?"

L’inheritance multiple (en abrégé MI) sent , ce qui signifie que d’ habitude , cela a été fait pour de mauvaises raisons et que cela se répercutera sur le responsable.

Résumé

  1. Envisager la composition des caractéristiques, au lieu de l’inheritance
  2. Méfiez-vous du diamant de l’effroi
  3. Considérer l’inheritance de plusieurs interfaces au lieu d’objects
  4. Parfois, l’inheritance multiple est la bonne chose. Si c’est le cas, alors utilisez-le.
  5. Préparez-vous à défendre votre architecture à inheritance multiple dans les revues de code

1. Peut-être la composition?

Cela est vrai pour l’inheritance, et c’est encore plus vrai pour l’inheritance multiple.

Votre object doit-il vraiment hériter d’un autre? Une Car n’a pas besoin d’hériter d’un Engine pour fonctionner, ni d’une Wheel . Une Car a un Engine et quatre Wheel .

Si vous utilisez l’inheritance multiple pour résoudre ces problèmes au lieu de la composition, vous avez fait quelque chose de mal.

2. Le diamant de l’effroi

Habituellement, vous avez une classe A , alors B et C héritent tous deux 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 huit ans et c’est amusant de voir à cause de:

  1. Combien d’erreur était-il depuis le début (Dans les deux cas, D n’aurait pas dû hériter des deux C et B ), car c’était une mauvaise architecture (en fait, C n’aurait pas du tout existé …)
  2. Combien de mainteneurs payaient-ils pour cela, car en C ++, la classe parente A était présente deux fois dans sa classe D , et donc, mettre à jour un champ parent A::field signifiait soit la mettre à jour deux fois (via B::field et C::field ), ou avoir quelque chose qui ne va pas et se bloquer plus tard (nouveau un pointeur dans B::field , et supprimer C::field …)

Utiliser le mot-clé virtual en C ++ pour qualifier l’inheritance évite la double présentation 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’objects, vous devez essayer de conserver la hiérarchie en tant qu’arborescence (un nœud possède 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 le design est sain – consultez votre code! ), C’est que 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 par tous les moyens, héritez-en deux fois.
  • s’il ne devait exister qu’une seule fois, héritez-en virtuellement.

Ce choix est inhérent au problème, et en C ++, contrairement aux autres langages, vous pouvez réellement le faire sans dogme forçant votre conception au niveau de la langue.

Mais comme tous les pouvoirs, avec ce pouvoir vient la responsabilité: faites réviser votre conception.

3. Interfaces

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

Généralement, ce que vous voulez dire lorsque C hérite de A et B est que les utilisateurs peuvent utiliser C comme s’il s’agissait d’un A et / ou comme s’il s’agissait d’un B

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

  1. toute sa méthode déclarée pure virtuelle (suffixée par = 0) (supprimé le 2017-05-03)
  2. aucune variable membre

L’inheritance multiple de zéro pour un object réel et zéro ou plusieurs interfaces n’est pas considéré comme “malodorant” (du moins pas autant).

Plus d’informations 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 n’avoir aucun état (c’est-à-dire, aucune variable membre, sauf this ). Votre interface abstraite a pour but de publier un contrat (“vous pouvez m’appeler de cette manière, et de cette manière”), rien de plus, rien de moins. La limitation à avoir uniquement une méthode virtuelle abstraite devrait être un choix de conception, pas une obligation.

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

Troisièmement, l’orientation des objects est excellente, mais ce n’est pas la seule vérité qui existe en C ++. Utilisez les bons outils et rappelez-vous toujours que vous avez d’autres paradigmes en C ++ offrant différents types de solutions.

4. Avez-vous vraiment besoin de l’inheritance multiple?

Parfois oui.

Habituellement, votre classe C hérite de A et B , et A et B sont deux objects non liés (c.-à-d. Pas dans la même hiérarchie, rien en commun, différents concepts, etc.).

Par exemple, vous pourriez avoir un système de Nodes avec des coordonnées X, Y, Z, capable d’effectuer beaucoup de calculs géomésortingques (peut-être un point, une partie d’objects géomésortingques) et chaque nœud est un agent automatisé, capable de communiquer avec d’autres agents. .

Peut-être avez-vous déjà access à deux bibliothèques, chacune avec son propre espace de noms (une autre raison d’utiliser les espaces de noms … Mais vous utilisez des espaces de noms, n’est-ce pas?)

Vous avez donc votre propre own::Node dérivant à la fois de ai::Agent et de 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 le fera pas.

Ensuite, vous aurez besoin de plusieurs inheritances, en ayant votre own::Node Communiquer avec d’autres agents en fonction de leur position dans un espace 3D.

(Vous remarquerez que ai::Agent et geo::Point sont totalement, totalement, totalement NON RELIGIEUX … Cela réduit considérablement le risque d’inheritance multiple)

Autres cas (modifier 2017-05-03)

Il y a d’autres cas:

  • Utilisation de l’inheritance (espérons-le, privé) comme détail d’implémentation
  • Certains idiomes C ++, comme les stratégies, peuvent utiliser l’inheritance multiple (lorsque chaque partie doit communiquer avec les autres)
  • l’inheritance virtuel de std :: exception (l’ inheritance virtuel est-il nécessaire pour les exceptions? )
  • etc.

Parfois, vous pouvez utiliser la composition, et parfois MI est mieux. Le point est le suivant: vous avez le choix. Faites-le de manière responsable (et faites examiner votre code).

5. Alors, dois-je faire plusieurs inheritances?

La plupart du temps, selon mon expérience, non. MI n’est pas le bon outil, même s’il semble fonctionner, car il peut être utilisé par les fainéants pour emstackr des entités sans en connaître les conséquences (comme faire une Car fois comme Engine et comme Wheel ).

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

Mais parce que MI est malodorant, soyez prêt à défendre votre architecture dans les revues de code (et le défendre est une bonne chose, car si vous ne pouvez pas le défendre, vous ne devriez pas le faire).

D’après une interview avec Bjarne Stroustrup :

Les gens disent à juste titre que vous n’avez pas besoin de l’inheritance multiple, car vous pouvez également faire tout ce que vous pouvez faire avec l’inheritance multiple avec un seul inheritance. Vous utilisez juste le tour de délégation que j’ai mentionné. De plus, vous n’avez besoin d’aucun inheritance, car tout ce que vous faites avec un seul inheritance peut également être fait sans transfert en passant par 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 pratique d’utiliser les installations linguistiques? Quand préférez-vous une solution de contournement? J’ai vu des cas où l’inheritance multiple est utile, et j’ai même vu des cas où l’inheritance multiple assez compliqué est utile. Généralement, je préfère utiliser les facilités offertes par la langue pour contourner les problèmes

Il n’y a aucune raison de l’éviter et cela peut être très utile dans les 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 pensé à cela et vous permet de faire un inheritance virtuel pour contourner 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 de ne pas utiliser l’inheritance pour économiser sur la réutilisation des données. Si vous pouvez représenter la même chose avec la composition (et vous pouvez généralement le faire), cette approche est bien meilleure.

Voir w: Héritage multiple .

L’inheritance multiple a été critiqué et, en tant que tel, n’est pas implémenté dans de nombreuses langues. Les critiques incluent:

  • Complexité accrue
  • Ambiguïté sémantique souvent résumée comme le problème du diamant .
  • Ne pas pouvoir hériter explicitement plusieurs fois d’une seule classe
  • Ordre d’inheritance changeant la sémantique de classe.

L’inheritance multiple dans les langages avec des constructeurs de style C ++ / Java aggrave le problème d’inheritance des constructeurs et du chaînage des constructeurs, créant ainsi des problèmes de maintenance et d’extensibilité dans ces langages. Les objects dans les relations d’inheritance avec des méthodes de construction très variables sont difficiles à implémenter sous le paradigme de chaînage du constructeur.

Un moyen moderne de résoudre ce problème pour utiliser une interface (pure classe abstraite) comme les interfaces COM et Java.

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

Oui, vous pouvez. Je vais voler au GoF .

  • Programme à une interface, pas une implémentation
  • Préférer la composition à l’inheritance

L’inheritance 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 de l’inheritance est assez superficielle (comme cela devrait être presque toujours le cas) et bien gérée, il est peu probable que vous obteniez l’inheritance redoutable du diamant. Le diamant n’est pas un problème avec toutes les langues qui utilisent l’inheritance multiple, mais le traitement de C ++ est souvent maladroit et parfois déroutant.

Bien que j’ai rencontré des cas où l’inheritance multiple est très pratique, ils sont en réalité 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 de l’inheritance multiple. Je préfère éviter les constructions de langage confuses, et il est facile de construire des cas d’inheritance dans lesquels il faut vraiment lire le manuel pour comprendre ce qui se passe.

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

Vous devriez l’utiliser avec prudence, il y a des cas, comme le problème du diamant , où les choses peuvent se compliquer.

alt text http://soffr.miximages.com/c%2B%2B/PoweredDevice.gif

Chaque langage de programmation a un traitement légèrement différent de la programmation orientée object avec des avantages et des inconvénients. 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 invalide – et c’est le cas de l’inheritance multiple. En conséquence, il y a une tendance à éloigner les programmeurs de cette fonctionnalité.

D’autres personnes ont abordé la question de savoir ce que l’inheritance multiple n’est pas bon. Mais nous avons vu pas mal de commentaires qui impliquent plus ou moins que la raison de l’éviter est 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 que vous distinguiez un type particulier de définition de classe appelé un “mix-in”; class est un mix-si toutes ses fonctions membres sont virtuelles (ou virtuelles). Ensuite, vous êtes autorisé à hériter d’une seule classe principale et autant de “mix-ins” que vous le souhaitez – mais vous devez hériter des 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 avez l’intention d’utiliser une classe en tant que classe de mixage, vous adoptez également une convention de dénomination pour faciliter à quiconque lit le code de voir ce qui se passe et de vérifier que vous jouez avec les règles de base . Et vous constaterez que cela fonctionne beaucoup mieux si vos mix-ins ont aussi des constructeurs par défaut, simplement à cause du fonctionnement des classes de base virtuelles. Et rappelez-vous 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 template paramétrée (voir ce lien pour une bonne explication) mais je pense que c’est une utilisation correcte de la terminologie.

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

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

Si nous pensons à toutes nos classes et à toutes les flèches entre elles qui dénotent des relations d’inheritance, alors quelque chose comme ça

 A --> B 

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

 A --> B, B --> C 

nous disons que C dérive de B qui dérive de A, donc on dit que C dérive de A, donc

 A --> C 

De plus, nous disons que pour chaque classe A dérivée sortingvialement de A , notre modèle d’inheritance répond à la définition d’une catégorie. Dans un langage plus traditionnel, nous avons une catégorie Class avec des objects toutes les classes et tous les morphismes des relations d’inheritance.

C’est un peu la configuration, mais avec cela, jetons un coup d’oeil à notre Diamond of Doom:

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

C’est un diagramme à l’ombre, mais ça ira. Donc, D hérite de tous les A , B et C De plus, et en se rapprochant de la question de OP, D hérite également de toute super-classe 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 les suivants: C et B partagent certains noms de propriété / méthode et les choses deviennent ambiguës; cependant, si nous déplaçons un comportement partagé dans A l’ambiguïté disparaît.

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 sous-classe de Q Cela fait quelque chose appelé A pushout .

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

En vous assurant que les pointes du diamant sont des retraits et des poussées, cela nous permet de gérer de manière générique les problèmes de conflit de nom ou d’entretien qui pourraient survenir autrement.

Notez que la réponse de Paercebal a inspiré ceci car ses admonestations sont impliquées 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 les relations d’inheritance multiples peuvent être à la fois puissantes et non problématiques.

TL; DR Considérez les relations d’inheritance dans votre programme comme formant une catégorie. Ensuite, vous pouvez éviter les problèmes liés au Diamond of Doom en générant des poussées de classes héritées de façon multiple et de manière symésortingque, en créant une classe parente commune qui constitue un retrait.

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, il est plus utile que les gens ne le réalisent parce qu’ils sont: A) dans une langue dangereuse qui ne le gère pas bien – OU – B) satisfaits de leur travail pendant des années et des années – OU – C) d’autres raisons ( trop nombreux pour les énumérer, je suis tout à fait sûr – voir les réponses ci-dessus.

Pour nous, utiliser Eiffel, MI est aussi naturel que toute autre chose et un autre bon outil dans la boîte à outils. Franchement, nous ne sums pas du tout préoccupés par le fait que personne n’utilise Eiffel. Pas de soucis. Nous sums heureux de ce que nous avons et nous vous invitons à regarder.

Pendant que vous regardez: Prenez note de la sécurité du vide et de l’éradication du déréférencement du pointeur Null. Pendant que nous dansons tous autour de MI, vos pointeurs se perdent! 🙂

Usages et abus de l’inheritance.

L’article fait un excellent travail pour expliquer l’inheritance et ses dangers.

Au-delà de la structure en losanges, l’inheritance multiple tend à rendre le modèle object plus difficile à comprendre, ce qui augmente les coûts de maintenance.

La composition est insortingnsèquement facile à comprendre, à comprendre et à expliquer. Il peut être fastidieux d’écrire du code, mais un bon IDE (cela fait quelques années que j’ai travaillé avec Visual Studio, mais les IDE Java ont tous d’excellents outils d’automatisation des raccourcis de composition).

En outre, en termes de maintenance, le “problème du diamant” se pose également dans les instances d’inheritance non littérales. Par exemple, si vous avez A et B et que votre classe C les étend tous les deux, et que A a une méthode makeJuice qui fait du jus d’orange et que vous étendez cela pour faire du jus d’orange avec une touche de chaux: B ‘ajoute une méthode’ makeJuice ‘qui génère un courant élecsortingque? “A” et “B” peuvent être compatibles “parents” en ce moment , mais cela ne signifie pas qu’ils le seront toujours!

Dans l’ensemble, la maxime consistant à éviter l’inheritance, en particulier l’inheritance multiple, est saine. Comme toutes les maximes, il y a des exceptions, mais vous devez vous assurer qu’il y a un néon vert clignotant à toutes les exceptions que vous codez (et entraînez votre cerveau afin que chaque fois que vous voyez de tels arbres, vous dessinez votre néon vert clignotant). signe), et que vous vérifiez pour que tout soit logique de temps en temps.

Le principal problème avec MI des objects concrets est que vous avez rarement un object qui doit légitimement “être un A et être un B”, donc c’est rarement la solution correcte pour des raisons logiques. Plus souvent, vous avez un object C qui obéit à “C peut agir comme un A ou un B”, ce que vous pouvez obtenir via l’inheritance et la composition de l’interface. Mais ne vous méprenez pas, l’inheritance de plusieurs interfaces est toujours MI, juste un sous-ensemble.

Pour C ++ en particulier, la principale faiblesse de la fonctionnalité n’est pas l’existence réelle de l’inheritance multiple, mais certaines des constructions qu’elle autorise sont presque toujours mal formées. Par exemple, l’inheritance de plusieurs copies du même object comme:

 class B : public A, public A {}; 

est mal formé PAR DEFINITION. Traduit en anglais, c’est “B est un A et un A”. Donc, même en langage humain, il y a une ambiguïté grave. Voulez-vous dire “B a 2 As” ou simplement “B est un A” ?. Autoriser un tel code pathologique, et pire encore, en faire un exemple d’utilisation, n’offrait aucune faveur à C ++ lorsqu’il s’agissait de défendre la fonctionnalité dans les langages successeurs.

Vous pouvez utiliser la composition de préférence à l’inheritance.

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

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

Cela pourrait ne jamais être un problème, mais si un jour vous avez une structure de microdonnées qui est instanciée des milliards de fois, ce sera le cas.