Cerca nel sito:
ricerca
avanzata

Frasi Celebri...

La mente umana tratta una nuova idea allo stesso modo di come il corpo tratta una proteina estranea -- la rigetta.

P. Medawar 

Sondaggio:

Qual'? il migliore tra i pi? recenti allenatori della nazionale?

Trapattoni
Zoff
Sacchi
Vicini
Maldini

visualizza risultati


 

Limiti e regole del polimorfismo

 

Esistono limitazioni e regole connesse all'uso delle funzioni virtuali legate piu` o meno direttamente al funzionamento del polimorfismo; vediamole in dettaglio.

Una funzione membro virtuale non puo` essere dichiarata static: i metodi virtuali sono fortemente legati alle istanze, mentre i metodi statici sono legati alle classi.

Se una classe base dichiara un metodo virtual, tale metodo restera` sempre virtual in tutte le classi derivate anche quando la classe derivata lo ridefinisce senza dichiararlo esplicitamente virtual. Scrivere cioe`


  class Base {
    public:
      virtual void Foo();
  };

  class Derived: public Base {
    public:
      void Foo();
  };


e` lo stesso che scrivere


  class Base {
    public:
      virtual void Foo();
  };

  class Derived: public Base {
    public:
      virtual void Foo();
  };


Entrambe le forme sono lecite, possiamo cioe` omettere virtual all'interno delle classi derivate quando vogliamo ridefinire un metodo ereditato.

E` possibile che una classe derivata ridefinisca virtual un membro che non lo era nella classe base. In questo caso il comportamento del compilatore puo` sembrare strano; vediamo un esempio:


  #include < iostream >
  using std::cout;

  class Base {
    public:
      void Foo() {
        cout << "Base::Foo()" << endl;
      }
  };

  class Derived1: public Base{
    public:
      virtual void Foo(){
        cout << "Derived1::Foo()" << endl;
      }
  };

  class Derived2: public Derived1 {
    public:
      virtual void Foo(){
        cout << "Derived2::Foo()" << endl;
      }
  };

  int main(int, char* []) {
    Base* BasePtr = new Derived1;
    Derived* DerivedPtr = new Derived2;
    BasePtr -> Foo();
    DerivedPtr -> Foo();
    return 0;
  }


Eseguendo il precedente codice, otterreste questo output:


  Base::Foo()
  Derived2::Foo()


Essendo BasePtr un puntatore alla classe base, il compilatore non puo` forzare il late binding perche` l'istanza puntata potrebbe essere effettivamente di tipo Base; per non rischiare una operazione che potrebbe mandare in crash il sistema, il compilatore adotta un approccio conservativo e chiama sempre Base::Foo() (al piu` il programma non fara` la cosa giusta, ma l'integrita` del resto del sistema non sara` compromessa). Nel secondo caso, poiche` DerivedPtr puo` puntare solo a istanze di Derived1 o sue sottoclassi, viene eseguito regolarmente il late binding perche` il metodo Foo() sara` sempre virtual.
Da questo comportamento deriva un suggerimento ben preciso: se una classe potrebbe in futuro essere derivata (se pensate cioe` che ci siano validi motivi per specializzarla ulteriormente), e` bene che i metodi dell'interfaccia (potenzialmente soggetti a ridefinizione) siano sempre virtual, onde evitare potenziali errori; d'altronde se non si usa il polimorfismo e` comunque possibile forzare la risoluzione del binding a compile time.

Non e` possibile avere costruttori virtuali. Il meccanismo che sta alla base del late binding richiede che l'istanza che si sta creando sia correttamente collegata alla sua VTABLE, ma il compito di settare correttamente il puntatore (VPTR) alla VTABLE spetta proprio al costruttore che di conseguenza non puo` essere dichiarato virtual. Questo problema avviamente non esiste per i distruttori, anzi e` bene che una classe che possieda metodi virtuali abbia anche il distruttore virtual in modo che distruggendo oggetti polimorfi venga sempre invocato il distruttore corretto.

Metodi virtuali chiamati dentro il costruttore o nel distruttore sono sempre risolti staticamente. Il motivo e` semplice e riconducibile all'ordine in cui sono chiamati costruttori e distruttori.
Consideriamo il seguente esempio:


  class Base {
    public:
      Base();
      ~Base();
      virtual void Foo();
  };

  Base::Base() {
    Foo();
  }

  Base::~Base() {
   Foo();
  }

  void Base::Foo() {
    /* ... */
  }

  class Derived: public Base {
    public:
      virtual void Foo();
  };

  void Derived::Foo() {
    /* ... */
  }


La costruzione di un oggetto Derived richiede che sia prima eseguito il costruttore di Base. Al momento in cui viene eseguita la chiamata a Foo() contenuta nel costruttore della classe base il puntatore alla VTABLE puo` al piu` puntare alla VTABLE della classe Base perche` il costruttore della classe derivata non e` ancora stato eseguito e non e` quindi possibile chiamare Derived::Foo(), si puo` chiamare solo la versione locale di Foo(). La situazione e` analoga nel caso dei distruttori, al momento in cui viene eseguito il distruttore della classe bae, il distruttore della classe derivata e` gia` stato eseguito ed il puntatore alla VTABLE non e` piu` valido; di conseguenza si puo` invocare solo Base::Foo().
Il suggerimento e` quindi quello di evitare per quanto possibile la chiamata di metodi virtuali all'interno di costruttori e distruttori, il risultato che potreste ottenere molto probabilmente non sarebbe quello desiderato.

Un potenziale errore legato all'uso di funzioni virtuali e` possibile quando una funzione membro richiama un'altra funzione membro virtuale. Consideriamo questo frammento di codice:


  class T {
    public:
      virtual void Foo();
      virtual void Foo2();
      void DoSomething();

    private:
      /* ... */
  };

  /* implementazione di T::Foo() e T::Foo2() */

  void T::DoSomething() {
    /* ... */
    Foo();
    /* ... */
    Foo2();
    /* ... */
  }


  class Td : public T {
    public:
      virtual void Foo2();
      void DoSomething();

    private:
      /* ... */
  };

  /* implementazione di Td::Foo2() */

  void Td::DoSomething() {
    /* ... */
    Foo();     // attenzione chiama T::Foo()
    /* ... */
    Foo2();
    /* ... */
  }


Si tratta di una situazione pericolosa: la classe Td ridefinisce un metodo non virtuale (ma poteva anche essere virtuale), ma non uno virtuale da questo richiamato. Di per se non si tratta di un errore, la classe derivata potrebbe non aver alcun motivo per ridefinire il metodo ereditato, tuttavia puo` essere difficile capire cosa esattamente faccia il metodo Td::DoSomething(), soprattutto in un caso simile:


  class Td2 : public Td {
    public:
      virtual void Foo();

    private:
      /* ... */
  };


Questa nuova classe ridefinisce un metodo virtuale, ma non quello che lo chiama, per cui in una situazione del tipo:


  Td2* Ptr = new Td2;
  /* ... */
  Ptr -> DoSomething();


viene chiamato il metodo Td::DoSomething() ereditato, ma in effetti questo poi chiama Td2::Foo() per via del linking dinamico.
Il risultato in queste situazioni e` che il comportamento che una classe puo` avere e` molto difficile da controllare e potrebbe essere potenzialmente errato; l'errore legato a situazioni di questo tipo e` noto in letteratura come fragile class problem e` puo` essere causa di forti inconsistenze.

Il polimorfismo e` uno strumento estremamente potente, tuttavia richiede una approfondita comprensione del suo funzionamente per essere utilizzato correttamente e in modo profiquo.

 

successivo
–«  INDICE  »–

 

 

 

 
Powered by paper&pencil (carta&matita ) - Copyright © 2001-2022 Cataldo Sasso