Quand l'héritage virtuel est un bon design?
EDIT3: assurez-vous de bien comprendre ce que je demande avant de répondre (il y a EDIT2 et beaucoup de commentaires autour). Il y a (ou avait) beaucoup de réponses qui montrent clairement une mauvaise compréhension de la question (je sais que c'est aussi de ma faute, désolé pour cela)
Salut, j'ai regardé les questions sur l'héritage virtuel (class B: public virtual A {...}
) en C++, mais je n'ai pas trouvé de réponse à ma question.
Je sais qu'il y a quelques problèmes avec l'héritage virtuel, mais ce que j'aimerais savoir est dans auquel cas l'héritage virtuel serait considéré comme un bon conception.
J'ai vu des gens mentionner des interfaces comme IUnknown
ou ISerializable
, et aussi que iostream
la conception est basée sur l'héritage virtuel. Serait-ce de bons exemples d'une bonne utilisation de l'héritage virtuel, est-ce juste parce qu'il n'y a pas de meilleure alternative, ou parce que l'héritage virtuel est la bonne conception dans ce cas? Grâce.
EDIT: pour clarifier, je pose des questions sur des exemples réels, veuillez ne pas donner abstraits. Je sais ce qu'est l'héritage virtuel et quel modèle d'héritage l'exige, ce que je veux savoir, c'est quand c'est la bonne façon de faire les choses et pas seulement une conséquence d'un héritage complexe.
EDIT2: en d'autres termes, je veux savoir quand la hiérarchie des diamants (qui est la raison de l'héritage virtuel) est un bon design
7 réponses
Si vous avez une hiérarchie d'interface et une hiérarchie d'implémentation correspondante, il est nécessaire de rendre les classes de base d'interface virtuelles.
Par exemple
struct IBasicInterface
{
virtual ~IBasicInterface() {}
virtual void f() = 0;
};
struct IExtendedInterface : virtual IBasicInterface
{
virtual ~IExtendedInterface() {}
virtual void g() = 0;
};
// One possible implementation strategy
struct CBasicImpl : virtual IBasicInterface
{
virtual ~CBasicImpl() {}
virtual void f();
};
struct CExtendedImpl : virtual IExtendedInterface, CBasicImpl
{
virtual ~CExtendedImpl() {}
virtual void g();
};
Généralement, cela n'a de sens que si vous avez un certain nombre d'interfaces qui étendent l'interface de base et plus d'une stratégie d'implémentation requise dans différentes situations. De cette façon vous avez une hiérarchie d'interface claire et vos hiérarchies d'implémentation peuvent utiliser l'héritage pour éviter la duplication de common application. Si vous utilisez Visual Studio, vous obtenez beaucoup D'avertissement C4250, cependant.
Pour éviter le découpage accidentel, il est généralement préférable que les classes CBasicImpl
et CExtendedImpl
ne soient pas instanciables mais aient un niveau d'héritage supplémentaire ne fournissant aucune fonctionnalité supplémentaire.
L'héritage virtuel est un bon choix de conception pour le cas où une classe A étend une autre Classe B, mais B n'a pas de fonctions de membre virtuel autres que éventuellement le destructeur. Vous pouvez penser à des classes comme B comme mixins , où une hiérarchie de types n'a besoin que d'une seule classe de base du type mixin pour en bénéficier.
Un bon exemple est l'héritage virtuel qui est utilisé avec certains des modèles iostream dans l'implémentation libstdc++ de la STL. Exemple, libstdc++ déclare le modèle basic_istream
avec:
template<typename _CharT, typename _Traits>
class basic_istream : virtual public basic_ios<_CharT, _Traits>
Il utilise l'héritage virtuel pour étendre basic_ios<_CharT, _Traits>
car istreams ne devrait avoir qu'une seule entrée streambuf, et de nombreuses opérations d'un istream devraient toujours avoir la même fonctionnalité (notamment la fonction membre rdbuf
pour obtenir la seule et unique entrée streambuf).
Imaginez maintenant que vous écrivez une classe (baz_reader
) qui s'étend std::istream
avec une fonction membre à lire dans les objets de type baz
, et une autre classe (bat_reader
) qui s'étend std::istream
avec un fonction membre à lire dans les objets de type bat
. Vous pouvez avoir une classe qui étend à la fois baz_reader
et bat_reader
. Si l'héritage virtuel n'était pas utilisé, les bases baz_reader
et bat_reader
auraient chacune leur propre entrée streambuf-probablement pas l'intention. Vous voudriez probablement que les bases baz_reader
et bat_reader
soient lues à la fois à partir du même streambuf. Sans héritage virtuel dans std::istream
pour étendre std::basic_ios<char>
, Vous pouvez accomplir cela en définissant les readbufs membres des bases baz_reader
et bat_reader
sur le même streambuf objet, mais alors vous auriez deux copies du pointeur sur le streambuf quand on suffirait.
Grrr .. L'héritage virtuel doit être utilisé pour le sous-typage d'abstraction. Il n'y a absolument aucun choix si vous devez obéir aux principes de conception de OO. Ne pas le faire empêche d'autres programmeurs de dériver d'autres sous-types.
Un exemple abstrait en premier: vous avez une abstraction de base A. Vous voulez créer un sous-type B. Veuillez noter que le sous-type signifie nécessairement une autre abstraction. Si ce n'est pas abstrait, c'est une implémentation pas un type.
Maintenant, un autre programmeur arrive et veut faire un sous-Type C DE A. Cool.
Enfin, un autre programmeur arrive et veut quelque chose qui est à la fois un B et un C. c'est aussi un a bien sûr. Dans ces scénarios, l'héritage virtuel est obligatoire.
Voici un exemple réel: à partir d'un compilateur, modéliser les types de données:
struct function { ..
struct int_to_float_type : virtual function { ..
struct cloneable : virtual function { ..
struct cloneable_int_to_float_type :
virtual function,
virtual int_to_float_type
virtual cloneable
{ ..
struct function_f : cloneable_int_to_float_type {
Ici function
représente les fonctions, int_to_float_type
représente un sous-type
composé de fonctions de int à float. Cloneable
est une propriété spéciale
que la fonction peut être cloné. function_f
est un béton (non-abstrait)
fonction.
Notez que si je n'ai pas fait à l'origine function
une base virtuelle de int_to_float_type
Je ne pouvais pas mixin cloneable
(et vice versa).
En général, si vous suivez le style POO "strict", vous définissez toujours un réseau d'abstractions, puis des implémentations sont dérivées pour elles. Vous séparez strictement sous-typing qui s'applique uniquement aux abstractions, et implémentation.
En Java, ceci est appliqué (les interfaces ne sont pas des classes). Dans C++ il n'est pas appliqué, et vous n'avez pas à suivre le modèle, mais vous devez en être conscient, et plus l'équipe avec laquelle vous travaillez est grande, ou le projet sur lequel vous travaillez, plus la raison pour laquelle vous devrez vous en écarter est forte.
Le typage Mixin nécessite beaucoup de ménage en C++. Dans Ocaml, les classes et les types de classes sont indépendants et assortis par la structure (possession de méthodes ou non), donc l'héritage est toujours une commodité. C'est en fait beaucoup plus facile à utiliser que le typage nominal. Les Mixins fournissent un moyen de simuler le typage structurel dans un langage qui n'a que le typage nominal.
L'héritage virtuel n'est pas une bonne ou une mauvaise chose - c'est un détail d'implémentation, comme tout autre, et il existe pour implémenter du code où les mêmes abstractions se produisent. C'est généralement la bonne chose à faire lorsque le code doit être super-runtime, par exemple, dans COM où certains objets COM doivent être partagés entre les processus, sans parler des compilateurs et autres, nécessitant L'utilisation de IUnknown où les bibliothèques C++ normales utiliseraient simplement shared_ptr
. En tant que tel, à mon avis, le code C++ normal devrait dépend de modèles et similaires et ne devrait pas nécessiter l'héritage virtuel, mais il est tout à fait nécessaire dans certains cas particuliers.
Puisque vous demandez des exemples spécifiques, Je suggérerai un comptage de références intrusif. Ce n'est pas que l'héritage virtuel est un bon design dans ce cas, mais l'héritage virtuel est le bon outil pour que le travail fonctionne correctement.
Au début des années 90, j'ai utilisé une bibliothèque de classes qui avait une classe ReferenceCounted
dont d'autres classes dériveraient pour lui donner un nombre de références et quelques méthodes pour gérer le nombre de références. L'héritage devait être virtuel, sinon si vous aviez plusieurs bases que chacune dérivait Non virtuellement de ReferenceCounted
, vous finiriez avec plusieurs comptes de référence. L'héritage virtuel vous assurait d'avoir un seul nombre de références pour vos objets.
Le comptage de référence non intrusif avec shared_ptr
et d'autres semble être plus populaire de nos jours, mais le comptage de référence intrusif est toujours utile lorsqu'une classe passe this
à d'autres méthodes. Les comptes de référence externes sont perdus dans ce cas. J'aime aussi que le comptage de référence intrusive dit à propos d'un classez comment le cycle de vie des objets de cette classe est géré.
[ je pense que je défends le comptage de référence intrusif parce que je le vois si rarement ces jours-ci, mais je l'aime.]
Héritage Virtuel est nécessaire lorsque vous êtes forcé utilisation de l'héritage multiple. Il y a quelques problèmes qui ne peuvent pas être résolus proprement/facilement en évitant l'héritage multiple. Dans ces cas (qui sont rares), vous devrez regarder l'héritage virtuel. 95% du temps, vous pouvez (et devriez) éviter l'héritage multiple pour vous sauver (et ceux qui regardent votre code après vous) de nombreux maux de tête.
Comme note de côté, COM ne vous force pas à utiliser l'héritage multiple. Il est possible (et assez commun) de créer un objet COM dérivé de IUnknown (directement ou indirectement) qui a un arbre d'héritage linéaire.