Riutilizzo del codice in C ++, tramite ereditarietà o composizione multipla? O…?

7

Originariamente avevo chiesto a questa domanda su StackOverflow, ma ero diretto qui, e penso che il mio problema sia tanto concettuale quanto tecnico, quindi ecco qui.

Se stai definendo una gerarchia di classi astratte in C ++ e poi creando sottoclassi concrete con implementazioni, potresti finire con classi astratte come questa, per esempio:

  A
 / \
B1 B2

Quindi le classi concrete quindi ereditano in questo modo:

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

E questo è tutto fine e dandy quando Cn e Dn stanno solo implementando le interfacce di Bn , o dove dicono C1 e C2 implementano l'interfaccia A in modo diverso .

Tuttavia, se voglio avere una funzionalità condivisa in C1 e C2 , che proviene dall'interfaccia A , dove la metto?

Non può andare in A , sia perché A è astratto e perché Dn dovrebbe non ereditarlo.

Sembra che esista un'implementazione di A_for_C teorico, ma questo appartiene a un'altra classe di antenati? O in una classe fratello composta?

   _____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)

Il primo sembra concettualmente accurato, ma richiede ereditarietà virtual , mentre il secondo richiede delega. Quindi entrambi impongono un successo in termini di prestazioni nonostante non ci sia alcuna reale ambiguità.

Leggendo sul web, su questo sito lo trovo detto

Some people believe that the purpose of inheritance is code reuse. In C++, this is wrong. Stated plainly, “inheritance is not for code reuse.”

In che modo quindi l'implementazione dovrebbe essere condivisa?

Ulteriori pensieri

Ho trovato alcune discussioni pertinenti in queste domande:

Penso che la risposta di utnapistim sotto sia molto più concisa e al punto di questi, e mi ha aiutato a tagliare mentalmente molte delle domande e delle risposte di queste altre domande.

La successione riguarda l'accettazione di un contratto. L'ereditarietà multipla va bene se la sottoclasse garantisce realmente l'adempimento dei contratti parent.

L'implementazione, tuttavia, è solo la vera preoccupazione dell'oggetto finale. Sì, potrebbe essere conveniente ereditare l'implementazione a volte, ma in realtà è ortogonale all'interfaccia e ci sono varie tecniche per implementare un'implementazione diversa dall'approccio basato su v-table predefinito, tra cui:

(Che sono, penso, equivalente tranne che uno è in fase di compilazione e uno è in fase di esecuzione.)

    
posta Leo 27.06.2014 - 19:41
fonte

2 risposte

6

Ci sono due aspetti alla tua domanda:

§1. Che cos'è l'ereditarietà c ++ (se non per il riutilizzo del codice)?

La risposta più semplice da dare qui è "l'implementazione di un contratto" (vedi anche principio di sostituzione di Liskov e pattern di progettazione di facciata ).

§2. How then should the implementation be shared?

Si consideri:

  • Incapsulamento di un oggetto, implementando un comportamento comune a entrambi i rami (questa è talvolta la migliore alternativa ai diamanti dell'ereditarietà). Il tuo A_for_C non ha bisogno di ereditare da A .
  • Funzioni gratuite (modelli se il tuo algoritmo si applica a più tipi).
  • Le classi di modelli ( CRTP potrebbero essere un'alternativa al modello di ereditarietà dei diamanti).
risposta data 27.06.2014 - 21:38
fonte
2

Penso che la maggior parte dei problemi di "ereditarietà" siano nel suo nome. Soprattutto quando si ha a che fare con C ++, che come linguaggio multiparadigm non deve necessariamente obbedire ai paradigmi OOP, schema e terminologia.

Se visti al di fuori di ogni terminologia specifica del paradigma, C ++ offre due metodi di "composizione" (con semplici annotazioni in inglese):

  • incorporamento esplicito ( struct A { B m; } a; a.m.fn(); // B::fn )
  • incorporamento implicito ( struct A: B {} a; a.fn(); // B::fn )

(Non ho usato "composizione" e "ereditarietà" solo per non "distrarre" la terminologia OOP)

Il secondo produce lo stesso layout della struttura dati del primo, rende semplicemente implicito il nome m e rende A un comportamento implicito come B. Se non facciamo in modo che la funzione virtuale entri in gioco, questo è solo un modo per importare il comportamento B definito in A senza la necessità di scrivere più codice in A stesso.

Non c'è intenzione di applicare alcun principio di sostituzione, qui. Non c'è alcun concetto OOP in questo. Questo non è quello che la scuola OOP chiama "eredità". Ha incidentalmente lo stesso nome dato dalla scuola OOP e dalla specifica di lag ++ di C ++.

std::true_type eredita da std::integal_constant . Nessuno di loro ha metodi virtuali (incluso il distruttore). Da purista OOP questa è una bestemmia, ma senza nervosismo, fa parte della libreria standard C ++.

Quando fai entrare in gioco la funzione virtuale, acquisisci la capacità di "scavalcare" un comportamento con un altro (può essere dichiarato come non definito). Questo rende le classi C ++ molto simili a quelle che la scuola OOP chiama "Object" e implicitamente incorporano ciò che chiamano "ereditarietà".

Ma il C ++ offre un altro "meccanismo di sostituzione" più spesso ignorato (insieme a funzioni virtuali): DOMINANCE. Spesso viene ignorato perché ha un significato minore con i linguaggi di ereditarietà singoli, ma riproduce in eredità multipla.

Considera questo:

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
{
};

Qui sia Actual_object e Another_object possono essere validi sostituti di Inteface_A , e poiché fn è pure (abstract, in altri letteratura) una chiamata a ia->fn() rende la chiamata a finire nell'unico metodo fn valido ereditato dall'oggetto derivato. Ed è quello fornito dalla classe ereditata " implementazione " (in puro senso C ++). Questo è dominace .

Puoi facilmente immaginarlo estesamente con un numero di interfacce diverse e una serie di "implementazioni parziali" diverse, che si ereditano l'un l'altro in un modo diverso, completando l'un l'altro fornendo comportamenti diversi da importare in un oggetto finale.

È OOP ortodosso? Assolutamente no. È "OOP valido"? rispetto all'interfaccia sì, rispetto alle implementazioni no (o almeno, non propriamente). È questo C ++ valido: sì. E anche un buon riutilizzo del codice, (nessuna implementazione da riscrivere molte volte in ogni oggetto che ha una stessa interfaccia da implementare) utilizzando il polimorfismo di tipo run-time, rimanendo fuori dai template e dal polimorfismo statico, non adeguato laddove diversi tipi di oggetto devono coesistono in uno stesso contesto di runtime: CRTP IA<A> e IA<B> sono entrambi denominati IA , hanno la stessa interfaccia, ma da una prospettiva di runtime non sono correlati. Non puoi avere un std::vector<something> che possa riferirli entrambi.

Qualcosa che la pura scuola OOP semplicemente non accetta solo perché ... non si fida dei propri programmatori per capire tutto questo. Ma per me ... è colpa loro, non di MI.

    
risposta data 15.08.2014 - 22:20
fonte

Leggi altre domande sui tag