web-dev-qa-db-fra.com

Code réutiliser en C ++, via plusieurs héritage ou composition? Ou...?

J'ai initialement demandé cette question sur Stackoverflow, mais j'étais dirigé ici, et je pense que mon problème est peut-être aussi conceptuel que technique, alors voilà.

Si vous définissez une hiérarchie de classe abstraite en C++, puis créez des sous-classes concrètes avec des implémentations, vous pourriez vous retrouver avec des classes abstraites quelque chose comme ça, par exemple:

  A
 / \
B1 B2

De sorte que les classes concrètes héritent alors comme:

B1   B2        B1   B2
 |    |         |    |
C1   C2        D1   D2

Et tout cela est bien et dandy lorsque Cn et Dn implémente les interfaces de Bn ou où dites C1 Et C2 Implémentez l'interface Adifféremment.

Cependant, si je veux avoir une fonctionnalité partagée dans C1 Et C2, Qui vient de l'interface A, où est-ce que je le mettez-le?

Il ne peut pas aller dans A, les deux parce que A est abstrait et parce que Dn devrait pas le hériter.

Il semble qu'il y ait un notionnel A_for_C Mise en œuvre, mais cela appartient-il dans une autre classe ancêtre? Ou dans une classe de soeur composée?

   _____A_____                       _____A_____
  /     |     \                     /     |     \
B1   A_for_C  B2        vs        B1     B2     A_for_C
 |_____/ \____ |                   |      |
C1            C2                  C1     C2

                               (C1 and C2 then each have an A_for_C and delegate)

Le premier semble conceptuellement précis, mais nécessite virtual héritage, tandis que la seconde requiert une délégation. Donc, tous deux imposent une performance touchée malgré la réelle ambiguïté.

Lire autour du web, sur ce site Web Je trouve que cela dit

Certaines personnes croient que le but de l'héritage est la réutilisation du code. En C++, c'est faux. Indiqué clairement, "l'héritage n'est pas pour la réutilisation du code".

Comment alors la mise en œuvre devrait-elle être partagée?

Principales pensées

J'ai trouvé une discussion pertinente dans ces questions:

Je pense que la réponse de Utnapismis ci-dessous est beaucoup plus collective et au point que celles-ci et m'a aidé mentalement coupé à travers ce que discuter de ces autres questions/réponses.

L'héritage consiste à accepter de remplir un contrat. Plusieurs héritages conviennent bien si la sous-classe garantit vraiment les contrats parents.

La mise en œuvre, cependant, n'est que vraiment la préoccupation de l'objet final. Oui, il peut être pratique d'hériter de la mise en œuvre, mais c'est en réalité orthogonal à l'interface, et il existe diverses techniques pour tirer dans une mise en œuvre autre que l'approche par défaut en V-Table, y compris:

(Qui sont, je pense, équivalent que celui-ci est compilé et que l'un est d'exécution.)

8
Leo

Votre question comporte deux aspects:

§1. Quel est le héritage C++ pour (si pas pour la réutilisation du code)?

La réponse la plus simple à donner ici est la "mise en œuvre d'un contrat" ​​(voir également principe de substitution de Liskov et modèle de conception de façade ).

§2. Comment alors la mise en œuvre devrait-elle être partagée?

Envisager:

  • Encapsulation d'un objet, mettant en œuvre un comportement commun aux deux branches (il s'agit parfois de la meilleure alternative aux diamants de héritage). Ton A_for_C n'a pas besoin d'hériter de A.
  • Fonctions libres (modèles si votre algorithme s'applique à plusieurs types).
  • Classes de modèles ( [~ # ~] CRTP [~ # ~ ~] Peut être une alternative à un motif de héritage de diamant).
6
utnapistim

Je pense que la plupart des problèmes "héritage" sont en son nom. Surtout lorsqu'il s'agit de C++, que comme une langue multiparadigm ne doit pas nécessairement obéir au OOP paradigmes, modèle et terminologie.

Si vous avez été mis de côté à partir de toute terminologie spécifique au paradigme, C++ offre deux méthodes de "composition" (avec une mémoire en anglais ordinaire):

  • intégration explicite (struct A { B m; } a; a.m.fn(); // B::fn)
  • intégration implicite (struct A: B {} a; a.fn(); // B::fn)

(Je n'ai pas utilisé de "composition" et "héritage" juste pour ne pas "distraire" dans OOP terminologie)

La seconde produise la même structure de structure de données la même structure de la structure de données que le premier, il fait simplement le nom m Nom implicite et permet de se comporter implicitement comme B. Si nous ne faisons pas de fonction virtuelle à entrer en jeu, c'est juste un moyen d'importer le comportement défini en B dans un sans la nécessité d'écrire plus de code dans une personne elle-même.

Il n'y a aucune intention d'appliquer un principe de substitution, ici. Il n'y a pas OOP concept dans ceci. Ce n'est pas ce que OOP Appel scolaire "Héritage". Il a juste d'ailleurs avoir le même nom donné par le =OOP School et la spécification C++ Laguage.

std::true_type Hérite de std::integal_constant. Aucun d'entre eux n'a de méthodes virtuelles (incluse le destructeur). De A OOP CURIST Ceci est un blasphème, mais nerveux, cela fait partie de la bibliothèque standard C++.

Lorsque vous effectuez une fonction virtuelle pour entrer en jeu, vous acquérez la capacité de "remplacer" un comportement avec un autre (peut être déclaré comme non défini). Cela rend les classes C++ à devenir très similaires à ce que OOP appelle l'école "objet" et implicite incorporant ce qu'ils appellent "héritage".

Mais C++ offre un autre "mécanisme de substitution" le plus souvent ignoré (ainsi que des fonctions virtuelles): la domination. Il est souvent ignoré car cela signifie moins que des langages de héritage unique, mais jouent dans plusieurs héritages.

Considère ceci:

struct Inteface_A
{
    virtual fnA() = 0;
    virtual ~Inteface_A() {};
};

struct Implementation_A_comon
{
    virtual fnA() { cout << "common implementation of IA" << endl; }
    virtual ~Implementation_A_comon() {}
};

struct Implementation_A_special
{
    virtual fnA() { cout << "special implementation of IA" << endl; }
    virtual ~Implementation_A_special() {}
};



class Actual_object:
   public Inteface_A,
   protected Implementation_A_comon,
   public Interface_B, //not declared here, but may be in another header
   protected Implementation_B_common // not declared here, may be in yet another header
{
};

class Another_object:
   public Inteface_A,
   protected Implementation_A_special
{
};

Ici tous les deux réal_object et autre_Object peut être un substitut valide de Inteface_A, Et comme fn est Pure (Résumé, dans d'autres littératures) Un appel à ia->fn() Donne l'appel à retrouver dans la seule méthode FN valide héritée de l'objet dérivé. Et c'est celui fourni par le " mise en œuvre" hérité de la classe (en C++ pure sens). C'est dominace.

Vous pouvez facilement imaginer cela étendu avec un certain nombre d'interfaces différentes et un certain nombre d'une "mise en oeuvre partielle différente", héritante d'une autre de manière variable, se complétant de fournir différents comportements à importer dans un objet final.

Est-ce orthodoxe OOP? Absolument pas. Est-ce "POOP valide"? Respect de l'interface Oui, respect des implémentations NO (ou au moins, pas correctement). Est-ce valide C++: Oui. Et aussi une bonne réutilisation de code (pas de mise en œuvre pour réécrire de nombreuses fois dans chaque objet qui a une même interface à mettre en œuvre) à l'aide de polymorphisme de type à temps d'exécution, séjourner à partir de modèles et de polymorphes statiques, pas adéquat où différents types d'objets doivent Coexistez dans un même contexte d'exécution: CRTPS IA<A> et IA<B> sont tous deux nommés IA, ont la même interface, mais à partir d'une perspective d'exécution ne sont pas liées. Vous ne pouvez pas avoir de std::vector<something> Pouvant les renvoyer à la fois.

Quelque chose du pur OOP School n'accepte tout simplement pas simplement parce que ... ils font confiance à leurs propres programmeurs dans la compréhension de tout cela. Mais pour moi ... c'est leur faute, pas des mi.

2
Emilio Garavaglia