Classi base virtuali

Il problema dell'ambiguita` che si verifica con l'ereditarieta` multipla, puo` essere portato al caso estremo in cui una classe ottenuta per ereditarieta` multipla erediti piu` volte una stessa classe base:

  class BaseClass {
    /* ... */
  };

  class Derived1 : public BaseClass {
    /* ... */
  };

  class Derived2 : private BaseClass {
    /* ... */
  };

  class Derived3 : public Derived1, private Derived2 {
    /* ... */
  };
Di nuovo quello che succede e` che alcuni membri (in particolare tutta una classe) sono duplicati nella classe Derived3 (anche se una copia di questi non e` immediatamente accessibile dalla classe derivata).
Consideriamo l'immagine in memoria di una istanza della classe Derived3, la situazione che avremmo sarebbe la seguente:

La classe Derived3 contiene una istanza di ciasciuna delle sue classi base dirette: Derived1 e Derived2.
Ognuna di esse contiene a sua volta una istanza della classe base BaseClass e opera esclusivamente su tale istanza.

In alcuni casi situazioni di questo tipo non creano problemi, ma in generale si tratta di una possibile fonte di inconsistenza.
Supponiamo ad esempio di avere una classe Person e di derivare da essa prima una classe Student e poi una classe Employee al fine di modellare un mondo di persone che eventualmente possono essere studenti o impiegati; dopo un po' ci accorgiamo che una persona puo` essere contemporaneamente uno studente ed un lavoratore, cosi` tramite l'ereditarieta` multipla deriviamo da Student e Employee la classe Student-Employee. Il problema e` che la nuova classe contiene due istanze della classe Person e queste due istanze vengono accedute (in lettura e scrittura) indipendentemente l'una dall'altra... Cosa accadrebbe se nelle due istanze venissero memorizzati dati diversi? Una gravissima forma di inconsistenza!
La soluzione viene chiamata ereditarieta` virtuale, e la si utilizza nel seguente modo:
  class Person {
    /* ... */
  };

  class Student : virtual private Person {
    /* ... */
  };

  class Employee : virtual private Person {
    /* ... */
  };

  class Student-Employee : private Student, private Employee {
    /* ... */
  };
Quando una classe eredita tramite la keyword virtual il compilatore non si limita a copiare il contenuto della classe base nella classe derivata, ma inserisce nella classe derivata un puntatore ad una istanza della classe base. Quando una classe eredita (per ereditarieta` multipla) piu` volte una classe base virtuale (e` questo il caso di Student-Employee che eredita piu` volte Person), il compilatore inserisce solo una istanza della classe virtuale e fa si che tutti i puntatori a tale classe puntino a quell'unica istanza.
La situazione in questo caso e` illustata dalla seguente figura:

La classe Student-Employee contiene ancora una istanza di ciasciuna delle sue classi base dirette: Student e Employee, ma ora esiste una sola istanza della classe base indiretta Person poiche` essa e` stata dichiarata virtual nelle definizioni di Student e Employee.

Il puntatore alla classe base virtuale non e` visibile al programmatore, non bisogna tener conto di esso poiche` viene aggiunto dal compilatore a compile-time, semplicemente si accede ai membri della classe base virtuale come si farebbe con una normale classe base.
Il vantaggio di questa tecnica e` che non e` piu` necessario definire la classe Student-Employee derivandola da Student (al fine di eliminare la fonte di inconsistenza) e aggiungendo a mano le definizioni di Employee, in tal modo si risparmiano tempo e fatica riducendo la quantita` di codice da produrre e limitando la possibilita` di errori e la quantita` di memoria necessaria al nostro programma per girare. C'e` pero` un costo da pagare: un livello di indirezione in piu` perche` l'accesso alle classi base virtuali (nell'esempio Person) avviene tramite un puntatore.
L'ereditarieta` virtuale genera anche un nuovo problema: il costruttore di una classe derivata chiama i costruttori delle classi base e nel caso di ereditarieta` virtuale una determinata classe base virtuale potrebbe essere inizializzata piu` volte.
Nel nostro esempio la classe base virtuale Person e` inizializzata sia da Student che da Employee, entrambe le classi hanno il dovere di eseguire la chiamata al costruttore della classe base, ma quando queste due classi vengono fuse per derivare la classe Student-Employee il costruttore della nuova classe, chiamando i costruttori di Student e Employee, implicitamente chiamerebbe due volte il costruttore di Person.
Per evitare tale comportamento e` stato deciso che i costruttori di una classe che possieda una classe base (diretta o indiretta) virtuale debbano eseguire esplicitamente una chiamata ad un costruttore della classe virtuale, il compilatore fara` poi in modo che l'inizializzazione sia effettivamente eseguita solo dalla classe massimamente derivata (ovvero quella cui appartiene l'istanza che si sta creando).
In questo modo ogni classe base virtuale e` inizializzata una sola volta e in modo deterministico.
Il seguente codice
  Person::Person() {
    cout << "Costruttore Person invocato..." << endl;
  }

  Student::Student() : Person() {
    cout << "Costruttore Student invocato..." << endl;
  }

  Employee::Employee() : Person() {
    cout << "Costruttore Employee invocato..." << endl;
  }

  Student-Employee::Student-Employee() : Person(), Student() {                                                                                 Employee() {
    cout << "Costruttore Student-Employee invocato..." << endl;
  }

  /* ... */

  cout << "Definizione di Tizio:" << endl;
  Person Tizio;
  cout << endl << "Definizione di Caio:" << endl;
  Student Caio;
  cout << endl << "Definizione di Sempronio:" << endl;
  Employee Sempronio;
  cout << endl << "Definizione di Bruto:" << endl;
  Student-Employee Bruto;
opportunamente completato, produrrebbe il seguente output:
    Definizione di Tizio:
    Costruttore Person invocato...

    Definizione di Caio:
    Costruttore Person invocato...
    Costruttore Student invocato...

    Definizione di Sempronio:
    Costruttore Person invocato...
    Costruttore Employee invocato...

    Definizione di Sempronio:
    Costruttore Person invocato...
    Costruttore Student invocato...
    Costruttore Employee invocato...
    Costruttore Student-Employee invocato...
Come potete osservare il costruttore della classe Person viene invocato una sola volta, per verificare poi da chi viene invocato basta tracciare l'esecuzione con un debugger simbolico.
Analogamente anche i distruttori seguono una regola simile, solo che in questo caso tutto il lavoro e` svolto dal compilatore (i distruttori non devono mai essere invocati esplicitamente).


Pagina precedente - Pagina successiva



C++, una panoramica sul linguaggio - © Copyright 1997, Paolo Marotta