125 votes

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

Je vois régulièrement des gens demander si l'héritage multiple devrait être inclus dans la prochaine version de C# ou de Java. Les adeptes du C++, qui ont la chance d'avoir cette capacité, disent que c'est comme donner une corde à quelqu'un pour qu'il finisse par se pendre.

Qu'en est-il de l'héritage multiple ? Existe-t-il des exemples concrets ?

56 votes

Je voudrais juste mentionner que C++ est excellent pour vous donner assez de corde pour vous pendre.

1 votes

Pour une alternative à l'héritage multiple qui aborde (et, IMHO, résout) un grand nombre des mêmes problèmes, regardez les Traits ( iam.unibe.ch/~scg/Recherche/Traits )

53 votes

Je pensais que C++ vous donnait suffisamment de marge de manœuvre pour vous tirer une balle dans le pied.

91voto

benjismith Points 8739

Le problème le plus évident est celui de la surcharge des fonctions.

Supposons que nous ayons deux classes A y B qui définissent tous deux une méthode doSomething . Vous définissez maintenant une troisième classe C qui hérite à la fois de A y B mais vous n'avez pas la possibilité de remplacer le doSomething méthode.

Lorsque le compilateur sème ce code...

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

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

Outre la surcharge, l'autre grand problème de l'héritage multiple est la disposition des objets physiques dans la mémoire.

Les langages tels que C++, Java et C# créent une disposition fixe basée sur l'adresse pour chaque type d'objet. C'est à peu près cela :

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 le code machine (ou bytecode), il utilise ces décalages numériques pour accéder à chaque méthode ou champ.

L'héritage multiple rend les choses très difficiles.

Si la classe C hérite à la fois de A y B le compilateur doit décider s'il faut disposer les données en AB dans l'ordre ou dans BA l'ordre.

Mais imaginez maintenant que vous appeliez des méthodes sur un fichier B objet. S'agit-il vraiment d'un B ? Ou s'agit-il en fait d'un C appelé de manière polymorphe, par l'intermédiaire de son objet B l'interface ? En fonction de l'identité réelle de l'objet, la disposition physique sera différente, et il est impossible de connaître l'offset de la fonction à invoquer sur le site d'appel.

La façon de gérer ce type de système est d'abandonner l'approche de la disposition fixe, en permettant à chaque objet d'être interrogé sur sa disposition avant en essayant d'invoquer les fonctions ou d'accéder à ses champs.

Donc... pour faire court... c'est un casse-tête pour les auteurs de compilateurs de prendre en charge l'héritage multiple. Ainsi, lorsque Guido van Rossum conçoit Python ou Anders Hejlsberg conçoit C#, ils savent que la prise en charge de l'héritage multiple va rendre l'implémentation du compilateur beaucoup plus complexe et, vraisemblablement, ils ne pensent pas que l'avantage en vaut le coût.

0 votes

Si vous mettez à jour votre réponse en décrivant en termes simples le problème évident que pose l'annulation, je me ferai un plaisir de sélectionner cette réponse comme acceptée.

27 votes

Ces arguments ne sont pas très convaincants - la question de la disposition fixe n'est pas du tout délicate dans la plupart des langages ; en C++, elle l'est parce que la mémoire n'est pas opaque et que l'on peut donc rencontrer des difficultés avec les hypothèses de l'arithmétique des pointeurs. Dans les langages où les classes définitions sont statiques (comme en Java, C# et C++), les conflits de noms d'héritages multiples peuvent être interdits à la compilation (et C# le fait de toute façon avec les interfaces !).

10 votes

L'OP souhaitait simplement comprendre les enjeux, et je les ai expliqués sans émettre d'opinion personnelle sur la question. J'ai simplement dit que les concepteurs de langages et les implémenteurs de compilateurs "ne pensent vraisemblablement pas que l'avantage en vaut le coût".

50voto

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

Par exemple, si vous héritez de A et B, qui ont tous deux la méthode foo(), vous ne voulez évidemment pas d'un choix arbitraire dans votre classe C qui hérite à la fois de A et de B. Vous devez soit redéfinir foo de façon à ce que ce qui sera fait soit clair. Vous devez soit redéfinir foo pour qu'il soit clair ce qui sera utilisé si c.foo() est appelé, soit renommer l'une des méthodes de C. (elle 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'il est utilisé partout et, personnellement, cette fonctionnalité m'a manqué lorsque j'ai dû revenir à la programmation en Java.

27voto

J Francis Points 970

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 existe une méthode dans A que B et C possèdent, il est possible de l'utiliser dans la classe D. surdéterminé et que D ne la surcharge pas, alors de quelle version de la méthode D hérite-t-il : 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 des classes dans cette situation. Dans ce cas, la classe A se trouve au sommet, les classes B et C se trouvent séparément en dessous, et la classe D réunit les deux en bas pour former un diamant...

4 votes

Qui dispose d'une solution connue sous le nom d'héritage virtuel. Ce n'est un problème que si l'on s'y prend mal.

1 votes

@IanGoldby : L'héritage virtuel est un mécanisme qui permet de résoudre une partie du problème, s'il n'est pas nécessaire d'autoriser des remontées et des descentes préservant l'identité parmi tous les types dont une instance est dérivée ou pour laquelle elle est substituable . Étant donné X:B ; Y:B ; et Z:X,Y ; supposons que quelqueZ est une instance de Z. Avec l'héritage virtuel, (B)(X)quelqueZ et (B)(Y)quelqueZ sont des objets distincts ; étant donné l'un ou l'autre, l'un pourrait obtenir l'autre via un downcast et un upcast, mais que se passe-t-il si l'on a une instance de Z ? someZ et veut l'envoyer à Object puis à B ? Quel est l'objet de la présente étude ? B sera-t-il obtenu ?

2 votes

@supercat Peut-être, mais les problèmes de ce type sont largement théoriques et peuvent de toute façon être signalés par le compilateur. L'important est d'être conscient du problème que l'on cherche à résoudre, puis d'utiliser le meilleur outil, en ignorant les dogmes de ceux qui préfèrent ne pas se préoccuper de comprendre "pourquoi".

21voto

KeithB Points 9459

L'héritage multiple fait partie de ces choses qui ne sont pas souvent utilisées, et qui peuvent être mal utilisées, mais qui sont parfois nécessaires.

Je n'ai jamais compris que l'on n'ajoute pas une fonctionnalité, simplement parce qu'elle pourrait être mal utilisée, lorsqu'il n'y a pas de bonnes alternatives. Les interfaces ne sont pas une alternative à l'héritage multiple. Tout d'abord, elles ne permettent pas d'imposer des préconditions ou des postconditions. Comme pour tout autre outil, vous devez savoir quand il convient de l'utiliser et comment l'utiliser.

0 votes

Pouvez-vous expliquer pourquoi ils ne vous permettent pas d'appliquer des conditions préalables et postérieures ?

2 votes

@Yttrill parce que les interfaces ne peuvent pas avoir d'implémentations de méthodes. Où mettez-vous le assert ?

1 votes

@curiousguy : vous utilisez un langage avec une syntaxe appropriée qui vous permet de mettre les pré et post conditions directement dans l'interface : pas besoin de "assert". Exemple de Felix : fun div(num : int, den : int when den != 0) : int expect result == 0 implies num == 0 ;

17voto

tloach Points 6590

Supposons que vous ayez des objets A et B qui sont tous deux hérités par C. A et B implémentent tous 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 genre de chose en est un important.

2 votes

Mais ce n'est pas vraiment un exemple concret. Si A et B ont tous deux 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().

0 votes

@Quantum : Et si ce n'est pas le cas ? Il est facile de voir le problème avec un seul niveau d'héritage, mais si vous avez beaucoup de niveaux et que vous avez une fonction aléatoire qui se trouve quelque part à deux reprises, cela devient un problème très difficile.

0 votes

De plus, le problème n'est pas que vous ne pouvez pas appeler la méthode A ou B en spécifiant laquelle vous voulez, le problème est que si vous ne le spécifiez pas, il n'y a pas de bonne façon de choisir. Je ne suis pas certain de la façon dont le C++ gère cela, mais si quelqu'un le sait, pourrait-il le mentionner ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X