Quel est le problème exact de l'héritage multiple?

121

Je peux voir des gens demander tout le temps si l'héritage multiple doit être inclus dans la prochaine version de C # ou Java. Les gens de C ++, qui ont la chance d'avoir cette capacité, disent que c'est comme donner à quelqu'un une corde pour finalement se pendre.

Quel est le problème avec l'héritage multiple? Y a-t-il des échantillons de béton?

Vlad Gudim
la source
54
Je voudrais juste mentionner que C ++ est idéal pour vous donner assez de corde pour vous pendre.
tloach
1
Pour une alternative à l'héritage multiple qui résout (et, à mon humble avis , résout) bon nombre des mêmes problèmes, regardez Traits ( iam.unibe.ch/~scg/Research/Traits )
Bevan
52
Je pensais que C ++ vous donne assez de corde pour vous tirer une balle dans le pied.
KeithB
6
Cette question semble supposer qu'il y a un problème avec MI en général, alors que j'ai trouvé beaucoup de langues où MI est utilisé occasionnellement. Il y a certainement des problèmes avec la gestion de MI dans certaines langues, mais je ne suis pas conscient que MI en général a des problèmes importants.
David Thornley

Réponses:

86

Le problème le plus évident est celui du remplacement de fonction.

Disons avoir deux classes Aet B, qui définissent toutes deux une méthode doSomething. Vous définissez maintenant une troisième classe C, qui hérite des deux Aet B, mais vous ne remplacez pas la doSomethingméthode.

Lorsque le compilateur amorce ce code ...

C c = new C();
c.doSomething();

... quelle implémentation de la méthode doit-elle utiliser? Sans plus de précision, il est impossible pour le compilateur de résoudre l'ambiguïté.

Outre le remplacement, l'autre gros problème avec l'héritage multiple est la disposition des objets physiques en mémoire.

Des langages tels que C ++, Java et C # créent une mise en page basée sur une adresse fixe pour chaque type d'objet. Quelque chose comme ça:

class A:
    at offset 0 ... "abc" ... 4 byte int field
    at offset 4 ... "xyz" ... 8 byte double field
    at offset 12 ... "speak" ... 4 byte function pointer

class B:
    at offset 0 ... "foo" ... 2 byte short field
    at offset 2 ... 2 bytes of alignment padding
    at offset 4 ... "bar" ... 4 byte array pointer
    at offset 8 ... "baz" ... 4 byte function pointer

Lorsque le compilateur génère du code machine (ou bytecode), il utilise ces décalages numériques pour accéder à chaque méthode ou champ.

L'héritage multiple le rend très délicat.

Si la classe Chérite des deux Aet B, le compilateur doit décider de mettre en page les données dans l' ABordre ou dans l' BAordre.

Mais maintenant, imaginez que vous appelez des méthodes sur un Bobjet. Est-ce vraiment juste un B? Ou s'agit-il en fait d'un Cobjet appelé polymorphiquement, via son Binterface? En fonction de l'identité réelle de l'objet, la disposition physique sera différente et il est impossible de connaître le décalage de la fonction à appeler sur le site d'appel.

La façon de gérer ce type de système est d'abandonner l'approche de mise en page fixe, permettant à chaque objet d'être interrogé pour sa mise en page avant d' essayer d'appeler les fonctions ou d'accéder à ses champs.

Donc ... longue histoire courte ... c'est une douleur dans le cou pour les auteurs de compilateurs de prendre en charge l'héritage multiple. Ainsi, quand quelqu'un comme Guido van Rossum conçoit python, ou quand Anders Hejlsberg conçoit c #, ils savent que la prise en charge de l'héritage multiple va rendre les implémentations du compilateur beaucoup plus complexes, et vraisemblablement, ils ne pensent pas que l'avantage en vaut le coût.

benjismith
la source
62
Ehm, Python soutient MI
Nemanja Trifunovic
26
Ce ne sont pas des arguments très convaincants - la mise en page fixe n'est pas du tout délicate dans la plupart des langues; en C ++, c'est délicat car la mémoire n'est pas opaque et vous pouvez donc rencontrer des difficultés avec les hypothèses arithmétiques des pointeurs. Dans les langages où les définitions de classe sont statiques (comme en java, C # et C ++), les conflits de noms d'héritage multiples peuvent être interdits lors de la compilation (et C # le fait de toute façon avec les interfaces!).
Eamon Nerbonne
10
Le PO voulait juste comprendre les problèmes, et je les ai expliqués sans éditer personnellement à ce sujet. Je viens de dire que les concepteurs de langage et les implémenteurs de compilateurs "ne pensent probablement pas que l'avantage en vaut le coût".
benjismith
12
« Le problème le plus évident est celui du remplacement de fonction. » Cela n'a rien à voir avec le remplacement de fonction. C'est un simple problème d'ambiguïté.
curiousguy
10
Cette réponse contient des informations erronées sur Guido et Python, car Python prend en charge MI. "J'ai décidé que tant que j'allais soutenir l'héritage, je pourrais tout aussi bien soutenir une version simple de l'héritage multiple." - Guido van Rossum python-history.blogspot.com/2009/02/… - De plus, la résolution d'ambiguïté est assez courante dans les compilateurs (les variables peuvent être locales pour bloquer, locales pour fonction, locales pour la fonction englobante, les membres d'objet, les membres de classe, globals, etc.), je ne vois pas en quoi une portée supplémentaire ferait une différence.
marcus
46

Les problèmes que vous mentionnez ne sont pas vraiment difficiles à résoudre. En fait, par exemple, Eiffel le fait parfaitement! (et sans introduire de choix arbitraires ou autre)

Par exemple, si vous héritez de A et B, tous deux ayant la méthode foo (), alors bien sûr, vous ne voulez pas d'un choix arbitraire dans votre classe C héritant à la fois de A et B. Vous devez soit redéfinir foo pour que ce soit clair. utilisé si c.foo () est appelé ou sinon vous devez renommer l'une des méthodes en C. (cela pourrait devenir bar ())

Je pense également que l'héritage multiple est souvent très utile. Si vous regardez les bibliothèques d'Eiffel, vous verrez qu'elle est utilisée partout et personnellement, j'ai manqué la fonctionnalité lorsque j'ai dû revenir à la programmation en Java.


la source
26
Je suis d'accord. La principale raison pour laquelle les gens détestent MI est la même qu'avec JavaScript ou avec le typage statique: la plupart des gens n'en ont jamais utilisé que de très mauvaises implémentations - ou l'ont très mal utilisé. Juger MI par C ++, c'est comme juger la POO par PHP ou juger les automobiles par Pintos.
Jörg W Mittag
2
@curiousguy: MI introduit encore un autre ensemble de complications dont il faut se soucier, tout comme la plupart des "fonctionnalités" de C ++. Ce n'est pas parce qu'il est clair qu'il n'est pas facile de travailler avec ou de déboguer. Suppression de cette chaîne car elle est sortie du sujet et vous l'avez quand même supprimée.
Guvante
4
@Guvante le seul problème avec MI dans n'importe quelle langue est que les programmeurs de merde pensent qu'ils peuvent lire un tutoriel et connaître soudainement une langue.
Miles Rout
2
Je dirais que les fonctionnalités linguistiques ne visent pas seulement à réduire le temps de codage. Il s'agit également d'augmenter l'expressivité d'une langue et d'augmenter les performances.
Miles Rout
4
De plus, les bogues ne se produisent à partir de MI que lorsque les idiots l'utilisent de manière incorrecte.
Miles Rout
27

Le problème du diamant :

une ambiguïté qui survient lorsque deux classes B et C héritent de A et que la classe D hérite à la fois de B et de C.S'il y a une méthode dans A que B et C ont écrasée , et D ne la remplace pas, alors quelle version du méthode D hérite-t-elle: celle de B, ou celle de C?

... On l'appelle le «problème du diamant» en raison de la forme du diagramme d'héritage de classe dans cette situation. Dans ce cas, la classe A est en haut, B et C séparément en dessous, et D joint les deux ensemble en bas pour former une forme de diamant ...

J Francis
la source
4
qui a une solution connue sous le nom d'héritage virtuel. Ce n'est un problème que si vous le faites mal.
Ian Gold du
1
@IanGoldby: L'héritage virtuel est un mécanisme pour résoudre une partie du problème, si l'on n'a pas besoin d'autoriser des hausses et des baisses de préservation de l'identité parmi tous les types dont une instance est dérivée ou pour lesquels elle est substituable . Étant donné X: B; Y: B; et Z: X, Y; Supposons que someZ est une instance de Z. Avec l'héritage virtuel, (B) (X) someZ et (B) (Y) someZ sont des objets distincts; étant donné l'un ou l'autre, on pourrait obtenir l'autre via un downcast et upcast, mais que se passe-t-il si l'on a un someZet veut le lancer vers Objectet ensuite vers B? Lequel Bobtiendra-t-il?
supercat du
2
@supercat Peut-être, mais des problèmes comme celui-là sont en grande partie théoriques, et dans tous les cas peuvent être signalés par le compilateur. L'important est d'être conscient du problème que vous essayez de résoudre, puis d'utiliser le meilleur outil, en ignorant le dogme des personnes qui préfèrent ne pas se préoccuper de comprendre «pourquoi?
Ian Gold du
@IanGoldby: Des problèmes comme celui-là ne peuvent être signalés par le compilateur que s'il a accès simultanément à toutes les classes en question. Dans certains frameworks, toute modification apportée à une classe de base nécessitera toujours une recompilation de toutes les classes dérivées, mais la possibilité d'utiliser des versions plus récentes des classes de base sans avoir à recompiler les classes dérivées (pour lesquelles on pourrait ne pas avoir de code source) est une fonctionnalité utile pour les cadres qui peuvent le fournir. De plus, les problèmes ne sont pas seulement théoriques. De nombreuses classes dans .NET reposent sur le fait qu'une Object
conversion
3
@IanGoldby: Très bien. Mon point était que les implémenteurs de Java et .NET n'étaient pas simplement "paresseux" en décidant de ne pas prendre en charge l'IM généralisée; soutenir l'IM généralisée aurait empêché leur cadre de soutenir divers axiomes dont la validité est plus utile à de nombreux utilisateurs que l'IM ne le serait.
supercat
21

L'héritage multiple est l'une de ces choses qui n'est pas souvent utilisée et qui peut être mal utilisée, mais qui est parfois nécessaire.

Je n'ai jamais compris de ne pas ajouter de fonctionnalité, simplement parce qu'elle pourrait être mal utilisée, alors qu'il n'y avait pas de bonnes alternatives. Les interfaces ne sont pas une alternative à l'héritage multiple. D'une part, ils ne vous permettent pas d'appliquer des conditions préalables ou des post-conditions. Tout comme tout autre outil, vous devez savoir quand il est approprié de l'utiliser et comment l'utiliser.

KeithB
la source
Pouvez-vous expliquer pourquoi ils ne vous permettent pas d'appliquer les conditions préalables et postérieures?
Yttrill
2
@Yttrill car les interfaces ne peuvent pas avoir d'implémentations de méthode. Où mettez-vous le assert?
curiousguy
1
@curiousguy: vous utilisez un langage avec une syntaxe adaptée qui vous permet de mettre les pré- et post-conditions directement dans l'interface: aucun "assert" n'est nécessaire. Exemple de Felix: fun div (num: int, den: int quand den! = 0): int expect result == 0 implique num == 0;
Yttrill
@Yttrill OK, mais certains langages, comme Java, ne supportent ni MI ni les "pré- et post-conditions directement dans l'interface".
curiousguy
Il n'est pas souvent utilisé car il n'est pas disponible et nous ne savons pas comment bien l'utiliser. Si vous regardez du code Scala, vous verrez comment les choses commencent à être courantes et peuvent être refactorisées en traits (Ok, ce n'est pas MI, mais cela prouve mon point).
santiagobasulto
16

disons que vous avez des objets A et B qui sont tous deux hérités par C. A et B implémentent tous les deux foo () et C ne le fait pas. J'appelle C.foo (). Quelle implémentation est choisie? Il y a d'autres problèmes, mais ce type de problème est important.

tloach
la source
1
Mais ce n'est pas vraiment un exemple concret. Si A et B ont une fonction, il est très probable que C aura également besoin de sa propre implémentation. Sinon, il peut toujours appeler A :: foo () dans sa propre fonction foo ().
Peter Kühne
@Quantum: Et si ce n'est pas le cas? Il est facile de voir le problème avec un niveau d'héritage, mais si vous avez beaucoup de niveaux et que vous avez une fonction aléatoire qui est quelque part deux fois, cela devient un problème très difficile.
tloach
En outre, le fait n'est pas que vous ne pouvez pas appeler la méthode A ou B en spécifiant celle que vous voulez, le fait est que si vous ne spécifiez pas, il n'y a pas de bon moyen d'en choisir une. Je ne sais pas comment C ++ gère cela, mais si quelqu'un sait, pourrait-il le mentionner?
tloach
2
@tloach - si C ne résout pas l'ambiguïté, le compilateur peut détecter cette erreur et renvoyer une erreur de compilation.
Eamon Nerbonne
@Earmon - En raison du polymorphisme, si foo () est virtuel, le compilateur pourrait même ne pas savoir au moment de la compilation que cela va être un problème.
tloach
5

Le principal problème de l'héritage multiple est bien résumé avec l'exemple de tloach. Lors de l'héritage de plusieurs classes de base qui implémentent la même fonction ou le même champ, le compilateur doit prendre une décision sur l'implémentation à hériter.

Cela s'aggrave lorsque vous héritez de plusieurs classes qui héritent de la même classe de base. (héritage du diamant, si vous dessinez l'arbre d'héritage, vous obtenez une forme de diamant)

Ces problèmes ne sont pas vraiment problématiques pour un compilateur à surmonter. Mais les choix que le compilateur doit faire ici sont plutôt arbitraires, ce qui rend le code beaucoup moins intuitif.

Je trouve que lorsque je fais une bonne conception OO, je n'ai jamais besoin d'héritage multiple. Dans les cas où j'en ai besoin, je trouve généralement que j'utilise l'héritage pour réutiliser des fonctionnalités alors que l'héritage n'est approprié que pour les relations "is-a".

Il existe d'autres techniques comme les mixins qui résolvent les mêmes problèmes et n'ont pas les problèmes de l'héritage multiple.

Mendelt
la source
4
Le compilé n'a pas besoin de faire un choix arbitraire - il peut simplement se tromper. En C #, quel est le type de ([..bool..]? "test": 1)?
Eamon Nerbonne
4
En C ++, le compilateur ne fait jamais de tels choix arbitraires: c'est une erreur de définir une classe où le compilateur devrait faire un choix arbitraire.
curiousguy
5

Je ne pense pas que le problème du diamant soit un problème, je considérerais que c'est du sophisme, rien d'autre.

Le pire problème, de mon point de vue, avec l'héritage multiple est RAD - les victimes et les gens qui prétendent être des développeurs mais en réalité sont coincés avec une demi-connaissance (au mieux).

Personnellement, je serais très heureux si je pouvais enfin faire quelque chose dans Windows Forms comme celui-ci (ce n'est pas du code correct, mais cela devrait vous donner l'idée):

public sealed class CustomerEditView : Form, MVCView<Customer>

C'est le principal problème que j'ai avec l'absence d'héritage multiple. Vous POUVEZ faire quelque chose de similaire avec les interfaces, mais il y a ce que j'appelle "s *** code", c'est ce douloureux c *** répétitif que vous devez écrire dans chacune de vos classes pour obtenir un contexte de données, par exemple.

À mon avis, il ne devrait y avoir absolument aucun besoin, pas le moindre, pour AUCUNE répétition de code dans un langage moderne.

Turing terminé
la source
J'ai tendance à être d'accord, mais seulement tendu: il y a un besoin d'une certaine redondance dans n'importe quelle langue pour détecter les erreurs. Quoi qu'il en soit, vous devriez rejoindre l'équipe de développeurs Felix car c'est un objectif fondamental. Par exemple, toutes les déclarations sont mutuellement récursives, et vous pouvez voir en avant comme en arrière afin que vous n'ayez pas besoin de déclarations en avant (la portée est définie, comme les étiquettes C goto).
Yttrill
Je suis complètement d'accord avec cela - je viens de rencontrer un problème similaire ici . Les gens parlent du problème des diamants, ils le citent religieusement, mais à mon avis, c'est si facile à éviter. (Nous n'avons pas tous besoin d'écrire nos programmes comme ils ont écrit la bibliothèque iostream.) L'héritage multiple devrait logiquement être utilisé lorsque vous avez un objet qui a besoin de la fonctionnalité de deux classes de base différentes qui n'ont pas de fonctions ou de noms de fonction qui se chevauchent. Entre de bonnes mains, c'est un outil.
jedd.ahyoung
3
@Turing Complete: sans avoir de répétition de code: c'est une bonne idée mais c'est incorrect et impossible. Il existe un grand nombre de modèles d'utilisation et nous souhaitons en extraire les plus courants dans la bibliothèque, mais il est insensé de les abstraire tous car même si nous le pouvions, la charge sémantique de se souvenir de tous les noms est trop élevée. Ce que vous voulez, c'est un bel équilibre. N'oubliez pas que la répétition est ce qui donne la structure aux choses (le motif implique la redondance).
Yttrill
@ lunchmeat317: Le fait que le code ne devrait généralement pas être écrit de telle manière que le «diamant» pose un problème, ne signifie pas qu'un concepteur de langage / framework peut simplement ignorer le problème. Si un cadre prévoit que la conversion ascendante et descendante préservent l'identité de l'objet, souhaite permettre aux versions ultérieures d'une classe d'augmenter le nombre de types pour lesquels elle peut être substituée sans que cela soit un changement radical, et souhaite permettre la création de type au moment de l'exécution, Je ne pense pas que cela pourrait permettre l'héritage de plusieurs classes (par opposition à l'héritage d'interface) tout en atteignant les objectifs ci-dessus.
supercat
3

Le Common Lisp Object System (CLOS) est un autre exemple de quelque chose qui prend en charge MI tout en évitant les problèmes de style C ++: l'héritage reçoit une valeur par défaut raisonnable , tout en vous laissant la liberté de décider explicitement comment, par exemple, appeler le comportement d'un super .

Frank Shearar
la source
Oui, CLOS est l'un des systèmes d'objets les plus supérieurs depuis la création de l'informatique moderne dans peut-être même depuis longtemps :)
rostamn739
2

Il n'y a rien de mal dans l'héritage multiple lui-même. Le problème est d'ajouter l'héritage multiple à un langage qui n'a pas été conçu avec l'héritage multiple dès le départ.

Le langage Eiffel prend en charge l'héritage multiple sans restrictions de manière très efficace et productive, mais le langage a été conçu à partir de là pour le supporter.

Cette fonctionnalité est complexe à implémenter pour les développeurs de compilateurs, mais il semble que cet inconvénient pourrait être compensé par le fait qu'un bon support d'héritage multiple pourrait éviter le support d'autres fonctionnalités (c'est-à-dire pas besoin d'interface ou de méthode d'extension).

Je pense que soutenir ou non l'héritage multiple est plus une question de choix, une question de priorités. Une fonctionnalité plus complexe prend plus de temps pour être correctement implémentée et opérationnelle et peut être plus controversée. L'implémentation C ++ peut être la raison pour laquelle l'héritage multiple n'a pas été implémenté en C # et Java ...

Christian Lemer
la source
1
Le support C ++ pour MI n'est pas " très efficace et productif "?
curiousguy
1
En fait, il est quelque peu cassé dans le sens où il ne correspond pas aux autres fonctionnalités de C ++. L'affectation ne fonctionne pas correctement avec l'héritage, encore moins l'héritage multiple (consultez les très mauvaises règles). Créer correctement des diamants est si difficile que le comité des normes a foiré la hiérarchie des exceptions pour la garder simple et efficace, plutôt que de le faire correctement. Sur un compilateur plus ancien que j'utilisais à l'époque, j'ai testé ceci et quelques mixins MI et implémentations d'exceptions de base coûtaient plus d'un mégaoctet de code et prenaient 10 minutes pour compiler .. juste les définitions.
Yttrill
1
Les diamants en sont un bon exemple. Dans Eiffel, le diamant est résolu explicitement. Par exemple, imaginez que l'étudiant et l'enseignant héritent tous deux de Personne. La personne a un calendrier, donc l'étudiant et l'enseignant hériteront de ce calendrier. Si vous créez un diamant en créant un TeachingStudent qui hérite à la fois de l'enseignant et de l'étudiant, vous pouvez décider de renommer l'un des calendriers hérités pour garder les deux calendriers disponibles séparément ou décider de les fusionner afin qu'il se comporte plus comme Personne. L'héritage multiple peut être mis en œuvre correctement, mais il nécessite une conception soignée et de préférence dès le début ...
Christian Lemer
1
Les compilateurs Eiffel doivent faire une analyse globale du programme pour implémenter ce modèle de MI efficacement. Pour les appels de méthode polymorphes, ils utilisent soit des thunks de répartiteur, soit des matrices creuses, comme expliqué ici . Cela ne se marie pas bien avec la compilation séparée de C ++ et la fonctionnalité de chargement de classe de C # et Java.
cyco130
2

L'un des objectifs de conception de frameworks tels que Java et .NET est de permettre au code qui est compilé de fonctionner avec une version d'une bibliothèque précompilée, de fonctionner aussi bien avec les versions ultérieures de cette bibliothèque, même si ces versions ultérieures ajouter de nouvelles fonctionnalités. Alors que le paradigme normal dans des langages comme C ou C ++ est de distribuer des exécutables liés statiquement qui contiennent toutes les bibliothèques dont ils ont besoin, le paradigme en .NET et Java est de distribuer les applications sous forme de collections de composants qui sont «liés» au moment de l'exécution .

Le modèle COM qui a précédé .NET a tenté d'utiliser cette approche générale, mais il n'avait pas vraiment d'héritage - au lieu de cela, chaque définition de classe définissait effectivement à la fois une classe et une interface du même nom qui contenait tous ses membres publics. Les instances étaient de type classe, tandis que les références étaient de type interface. Déclarer une classe comme dérivant d'une autre équivaut à déclarer une classe comme implémentant l'interface de l'autre, et oblige la nouvelle classe à réimplémenter tous les membres publics des classes dont l'un est dérivé. Si Y et Z dérivent de X, et alors W dérive de Y et Z, peu importe si Y et Z implémentent les membres de X différemment, car Z ne pourra pas utiliser leurs implémentations - il devra définir son posséder. W peut encapsuler des instances de Y et / ou Z,

La difficulté dans Java et .NET est que le code est autorisé à hériter des membres et y avoir accès se réfère implicitement aux membres parents. Supposons que l'on ait des classes WZ liées comme ci-dessus:

class X { public virtual void Foo() { Console.WriteLine("XFoo"); }
class Y : X {};
class Z : X {};
class W : Y, Z  // Not actually permitted in C#
{
  public static void Test()
  {
    var it = new W();
    it.Foo();
  }
}

Il semblerait que la W.Test()création d'une instance de W appelle l'implémentation de la méthode virtuelle Foodéfinie dans X. Supposons, cependant, que Y et Z étaient en fait dans un module compilé séparément, et bien qu'ils aient été définis comme ci-dessus lorsque X et W ont été compilés, ils ont ensuite été modifiés et recompilés:

class Y : X { public override void Foo() { Console.WriteLine("YFoo"); }
class Z : X { public override void Foo() { Console.WriteLine("ZFoo"); }

Maintenant, quel devrait être l'effet de l'appel W.Test()? Si le programme devait être lié statiquement avant la distribution, l'étape de liaison statique pourrait être en mesure de discerner que si le programme n'avait aucune ambiguïté avant que Y et Z ne soient modifiés, les modifications apportées à Y et Z ont rendu les choses ambiguës et l'éditeur de liens pourrait refuser de construire le programme à moins ou jusqu'à ce que cette ambiguïté soit résolue. D'un autre côté, il est possible que la personne qui possède à la fois W et les nouvelles versions de Y et Z soit quelqu'un qui souhaite simplement exécuter le programme et qui n'a aucun code source pour rien. Lorsqu'il W.Test()s'exécute, il ne serait plus clairW.Test() devrait faire, mais tant que l'utilisateur n'a pas essayé d'exécuter W avec la nouvelle version de Y et Z, aucune partie du système ne pourrait reconnaître qu'il y avait un problème (à moins que W ne soit considéré comme illégitime avant même les changements de Y et Z) .

supercat
la source
2

Le diamant n'est pas un problème, tant que vous n'utilisez rien comme l'héritage virtuel C ++: dans l'héritage normal, chaque classe de base ressemble à un champ membre (en fait, ils sont disposés dans la RAM de cette façon), vous donnant un peu de sucre syntaxique et un possibilité supplémentaire de remplacer davantage de méthodes virtuelles. Cela peut imposer une certaine ambiguïté au moment de la compilation, mais c'est généralement facile à résoudre.

D'un autre côté, avec l'héritage virtuel, il devient trop facilement incontrôlable (et devient alors un désordre). Prenons comme exemple un diagramme «cœur»:

  A       A
 / \     / \
B   C   D   E
 \ /     \ /
  F       G
    \   /
      H

En C ++, c'est totalement impossible: dès que Fet Gsont fusionnés en une seule classe, leurs As sont fusionnés aussi, point final. Cela signifie que vous ne pouvez jamais considérer les classes de base opaque en C ++ (dans cet exemple vous devez construire Aen Hsorte que vous devez savoir qu'il présente quelque part dans la hiérarchie). Dans d'autres langues, cela peut cependant fonctionner; par exemple, Fet Gpourrait explicitement déclarer A comme «interne», interdisant ainsi la fusion conséquente et se rendant effectivement solides.

Un autre exemple intéressant ( pas spécifique à C ++):

  A
 / \
B   B
|   |
C   D
 \ /
  E

Ici, Bn'utilise que l' héritage virtuel. EContient donc deux Bs qui partagent le même A. De cette façon, vous pouvez obtenir un A*pointeur qui pointe vers E, mais vous ne pouvez pas le convertir en B*pointeur bien que l'objet soit en fait en B tant que tel, le cast est ambigu, et cette ambiguïté ne peut pas être détectée au moment de la compilation (à moins que le compilateur ne voie le programme entier). Voici le code de test:

struct A { virtual ~A() {} /* so that the class is polymorphic */ };
struct B: virtual A {};
struct C: B {};
struct D: B {};
struct E: C, D {};

int main() {
        E data;
        E *e = &data;
        A *a = dynamic_cast<A *>(e); // works, A is unambiguous
//      B *b = dynamic_cast<B *>(e); // doesn't compile
        B *b = dynamic_cast<B *>(a); // NULL: B is ambiguous
        std::cout << "E: " << e << std::endl;
        std::cout << "A: " << a << std::endl;
        std::cout << "B: " << b << std::endl;
// the next casts work
        std::cout << "A::C::B: " << dynamic_cast<B *>(dynamic_cast<C *>(e)) << std::endl;
        std::cout << "A::D::B: " << dynamic_cast<B *>(dynamic_cast<D *>(e)) << std::endl;
        std::cout << "A=>C=>B: " << dynamic_cast<B *>(dynamic_cast<C *>(a)) << std::endl;
        std::cout << "A=>D=>B: " << dynamic_cast<B *>(dynamic_cast<D *>(a)) << std::endl;
        return 0;
}

De plus, l'implémentation peut être très complexe (dépend de la langue; voir la réponse de benjismith).

nombre zéro
la source
C'est le vrai problème avec MI. Les programmeurs peuvent avoir besoin de différentes résolutions au sein d'une même classe. Une solution à l'échelle du langage limiterait ce qui est possible et obligerait les programmeurs à créer des kludges pour que le programme fonctionne correctement.
shawnhcorey