[C++] Eredità a diamante

Super Squirrel
Ho letto del materiale sull'ereditarietà, ma non ho ben capito la parte (che viene subito prima dell'introduzione al polimorfismo) dove spiega l'eredità multipla e le classi basi virtuali.
In pratica se ho una classe C derivata da due classi B1 e B2 che a loro volta derivano da A, nel momento in cui creo un'istanza di C, la classe A verrà costruita due volte (da B1 e B2). Questa replicazione di una classe base può causare due generi di problemi: occupazione doppia di memoria e errori di ambiguità (se gli oggetti di C accedono direttamente ai membri ereditati da A il compilatore darebbe errore, non sapendo se accedere ai membri ereditati tramite B1 o tramite B2).
Entrambi i problemi si possono risolvere definendo A come classe base "virtuale" (realizzando la cosidetta eredità a diamante).
In sostanza la parola-chiave virtual dice a B1 e B2 di non prendersi cura di A quando viene creato un oggetto di C, perchè sarà la stessa classe "nipote" C ad occuparsi della sua "nonna".
Ho provato a scrivere un programmino per testare queste nozioni

#include <iostream>

using namespace std;

class A
{
protected:
    int valore;
    A(int x)
    {
        valore = x;
    }
public:
    int get_valore()
    {
        cout << valore;
    }
};

class B1 : virtual public A
{
public:
    B1(int x): A(x)
    {
        valore *= 2;
    }
};

class B2 : virtual public A
{
public:
    B2(int x): A(x)
    {
        valore *= 3;
    }
};

class C : public B1, B2
{
public:
    C(int x): B1(x), B2(x), A(x){}
};

int main()
{
    C c(1);
    c.get_valore();
    cout << endl;
    B1 b1(1);
    b1.get_valore();
    cout << endl;
    B2 b2(1);
    b2.get_valore();
}


Se creo istanze di B1 e B2 con argomento 1 mi dà giustamente 2 e 3, ma se creo un'istanza di C con argomento 1 mi dà 6... è come se seguisse entrambi i percorsi di ereditarietà ( 6 = 1 * 2 * 3), ma non capisco praticamente come ragiona il programma in questo caso.
Qualcuno potrebbe chiarirmi le idee al riguardo?
Dovrei forse aggiungere altro nel costruttore di C?

Risposte
Raptorista1
Credo che sia perché nella lista di inizializzazione chiami i costruttori di tutte le classi, e ad ogni chiamata corrisponde una modifica al membro [inline]valore[/inline].

Super Squirrel
Penso anche io ad una cosa del genere, ma vorrei capire concretamente come viene interpretato il codice dal compilatore.

Stando a quanto letto, nella lista di inizializzazione del costruttore di C devono essere incluse per forza le chiamate ai costruttori di B1, B2 e A. Inoltre se tolgo una sola delle chiamate il compilatore mi segnala errore.

Una domanda... da quanto ho letto, nel momento in cui una classe derivata viene istanziata, entrano in azione automaticamente i costruttori di tutte le classi gerarchicamente superiori, dalla classe base "capostipite" fino alla classe che deve creare l'oggetto. Ciò avviene perché, a partire dalla classe che deve creare l'oggetto, la lista di inizializzazione invoca il costruttore della classe genitrice finché non viene invocato il costruttore della classe base "capostipite", giusto?

Supponendo che ci abbia preso non riesco a capire cosa dovrebbe accadere nel caso del programmino da me postato. Mi spiego meglio, creo un'istanza della classe C e come prima cosa dovrebbe essere analizzata la lista di inizializzazione della classe C. Quindi stando alla logica e a quanto detto nel precedente post dovrebbe essere invocato il costruttore di A (valore = 1). E a questo punto cosa succede?

Spero che le premesse che ho fatto non siano sbagliate, altrimenti avrò scritto solo un mucchio di sciocchezze.

apatriarca
Il C++ è un linguaggio molto vasto e complesso e credo sia importante cercare di concentrarsi sulle funzionalità effettivamente usate e utili. Conosco il C++ da parecchi anni ormai e non ho MAI usato l'ereditarietà multipla e questo genere di funzionalità. Non credo di aver mai letto un codice in C++ reale (che non fosse insomma un esempio come quello da te mostrato) in cui venisse usata. Capisco il tuo desiderio di imparare i concetti fino in fondo, ma ci sono ben altre "priorità". Per esempio, com'è la tua conoscenza dei container standard? E' inoltre molto utile smettere di vedere solo piccoli esempi, ma cercare di scrivere programmi più realistici.

Super Squirrel
La mia conoscenza dei contenitori standard è quasi nulla. Studiando il linguaggio nè per lavoro nè per realizzare un progetto preciso, per il momento preferisco imparare il C++ "a basso livello".
Credo che apprese le basi in teoria si possano anche implementare dei contenitori come quelli della STL. Per esempio ho creato due classi template che dovrebbero funzionare in modo simile alla classe vector e alla classe list.

E' inoltre molto utile smettere di vedere solo piccoli esempi, ma cercare di scrivere programmi più realistici.


Ho letto qualcosa anche sul polimorfismo. Idee su qualche programma da fare per esercitarmi?

apatriarca
Non stai imparando il C++ "a basso livello". Stai solo perdendo tempo con funzionalità e dettagli ritenuti inutili dalla maggior parte degli sviluppatori C++. Già alla fine degli anni 90, quando l'ereditarietà era incredibilmente di moda ed era vista come la soluzione ad ogni problema, l'ereditarietà multipla era considerata dannosa e inutile. Questa è per esempio la ragione per cui Java non l'ha mai supportata. In effetti quasi nessun linguaggio permette l'ereditarietà multipla. Lascia perdere questi dettagli e impara qualcosa di più utile.

La libreria standard è qualcosa di molto utile. Se mai dovessi leggere un codice in C++, sarà molto più importante conoscere l'utilizzo di vector rispetto a sapere come implementare uno dei peggiori design mai creati (ereditarietà a diamante).

apatriarca
Spesso le idee descritte sul polimorfismo sono MOLTO datate.. Ma almeno ha più senso dell'ereditarietà multipla. Credo tuttavia che tu abbia comunque hai già imparato tutto quello che è necessariamente sapere sul polimorfismo. Che cosa credi di non aver capito/imparato?

Super Squirrel
Non credevo che l'ereditarietà multipla fosse una funzionalità tanto mal vista e così poco diffusa.

Per quanto riguarda il polimorfismo, credo di aver compreso gli argomenti trattati dal sito da cui sto studiando (funzioni virtuali, costruttori e distruttori virtuali, classi astratte), ma volevo fare pratica utilizzando i concetti appena imparati nell'implementazione di qualche programma.

A questo punto oltre ad approfondire la mia conoscenza dei contenitori standard e delle classi stream, credo che una buona idea sia quella di esercitarmi in qualche progetto un po' più corposo (pensavo tipo all'implementazione di un gioco di carte collezionabili).
Comunque qualsiasi consiglio su come dovrei strutturare il "percorso di studio" è ben accetto.

apatriarca
Prima di un gioco di carte collezionabili potresti ad esempio provare ad implementare un qualche tipo di gioco di carte più semplice (scopa, scala 40, pinnacola, UNO..).

Super Squirrel
Proverò con la scopa allora (visto che è uno dei pochi giochi di carte che conosco).

Rispondi
Per rispondere a questa discussione devi prima effettuare il login.