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

25
demandé sur Roman L 2011-01-05 18:07:01

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.

24
répondu CB Bailey 2011-01-05 16:03:32

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.

4
répondu Daniel Trebbien 2011-01-05 15:59:22

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.

1
répondu Yttrill 2011-01-05 16:28:50

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.

1
répondu Puppy 2011-01-05 16:40:32

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.]

1
répondu camh 2011-01-06 05:16:14

Ces FAQ répondaient à toutes les questions possibles liées à l'héritage virtuel. Même la réponse à votre question (si j'ai reconnu vos questions correctement ;) ) : FAQ 25.5

-1
répondu BЈовић 2011-01-06 06:47:23

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.

-3
répondu Zac Howland 2011-01-05 15:25:22