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, public Derived2 {
    /* ... */
  };


Di nuovo quello che succede e` che alcuni membri (in particolare tutta una classe) sono duplicati nella classe Derived3.
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 public Person {
    /* ... */
  };

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

  class Student-Employee : public Student,
                           public Employee {
    /* ... */
  };


Si tratta di un esempio che nella pratica non avrebbe alcuna validita`, ma ottimo da un punto di vista didattico.
Vediamo piu` in dettaglio cosa e cambiato e come virtual opera. 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 da 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` illustrata 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, tutto il meccanismo e` completamente trasparente, 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.
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 risolve dunque l'ambiguita` di cui sopra, nelle classi derivate le definizioni di una classe base virtuale sono presenti una volta sola, tuttavia il problema dell'ambiguita` non e` del tutto risolto, esistono ancora situazioni in cui il problema si ripropone. Supponiamo ad esempio che una delle classi intermedie ridefinisca una funzione membro della classe base:


  class Person {
    public:
      void DoSomething();
    /* ... */
  };

  class Student : virtual public Person {
    public:
      void DoSomething();
    /* ... */
  };

  class Employee : virtual public Person {
    public:
      void DoSomething();
    /* ... */
  };

  class Student-Employee : public Student,
                           public Employee {
    /* ... */
  };


Se Student-Employee non ridefinisce il metodo DoSomething(), la situazione seguente presenterebbe ancora ambiguita`:


  Student-Employee Caio;
  /* ... */
  Caio.DoSomething();       // Ambiguo!


perche` la classe Student-Employee eredita nuovamente due diverse definizioni del metodo DoSomething().
Esiste anche un caso apparentemente ambiguo e simile al precedente:


  class Person {
    public:
      void DoSomething();
    /* ... */
  };

  class Student : virtual public Person {
    public:
      void DoSomething();
    /* ... */
  };

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

  class Student-Employee : public Student,
                           public Employee {
    /* ... */
  };


La situazione e` pero` assai diversa, in questo caso solo una delle due classi base dirette ridefinisce il metodo ereditato da Person; in Student-Employee abbiamo ancora due definizioni di DoSomething(), ma una e` in un certo senso "piu` aggiornata" delle altre. In situazioni del genere si dice che Student::DoSomething() domina Person::DoSomething() e in questi casi ambiguita` tipo


  Student-Employee Caio;
  /* ... */
  Caio.DoSomething();


vengono risolte dal compilatore in favore della definizione dominante.
Si noti che ci deve essere una sola definizione che domina tutte le altre, altrimenti ci sarebbe ancora ambiguita`.

Ritorniamo a parlare a proposito della classe base virtuale.
Nei vari costruttori delle classi derivate c'e` implicitamente o esplicitamente una chiamata al costruttore della classe base virtuale, ma ora abbiamo una sola istanza di tale classe e non possiamo certo inizializzarla 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.
E` necessario stabilire un criterio deterministico che stabilisca chi deve inizializzare la classe virtuale. Lo standard stabilisce che il compito di inizializzare la classe base virtuale spetta alla classe massimamente derivata. La classe massimamente derivata e` quella che noi stiamo istanziando: se vogliamo creare un oggetto di tipo Student la classe massimamente derivata e` in questo caso Student, se invece stiamo istanziando Student-Employee allora e` quest'ultima la classe massimamente derivata.
E` dunque il costruttore della classe massimamente derivata che inizializza la classe virtuale.
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 Bruto:
  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.
Naturalmente ci sarebbe un problema simile anche con il distruttore, bisogna evitare che si tenti di distruggere la classe base virtuale piu` volte; nuovamente e` il compilatore che si assume l'onere di fare in modo che l'operazione venga eseguita una sola volta



Pagina precedente - Pagina successiva



C++, una panoramica sul linguaggio - seconda edizione
© Copyright 1996-1999, Paolo Marotta