Ereditarietà multipla
C++ method vs Twin-Objects
Daniela Briola
Orlin Velinov
Ereditarietà singola
 Ogni linguaggio Object Oriented
supporta il concetto di ereditarietà
A
 Si dice che una classe è derivata
da un’altra quando ne estende le
funzionalità grazie all’inheritance
 Nell’esempio la classe B è derivata
dalla classe A.
A
B
Ereditarietà multipla
 Permette di comporre classi derivando
da più classi base.
B
A
A
B
C
Ereditarietà multipla
Vantaggi
Svantaggi
 Possibilità di comporre
velocemente oggetti
molto complessi
 Complicazione notevole
del linguaggio che la
implementa
 Aggregare funzionalità
differenti in un’unica
classe
 Scarsa efficienza anche
quando non viene usata
 Soluzione elegante e di
grande utilità
 Rischio elevato di
“name clash”
Ereditarietà multipla:
il problema del diamante
 Se una classe eredita membri con lo
stesso nome da più di un genitore,
avviene un conflitto
Ci sono due strategie possibili:
 gestire direttamente il grafo di
ereditarietà
 trasformarlo in una catena lineare
(ereditarietà singola)
A
B1
B2
C
Ereditarietà multipla:
il problema del diamante
 La semantica dei linguaggi orientati
al grafo modella direttamente
l’albero di derivazione
 Se un membro è definito solo dalla
classe A, è ereditato da B1 e da C
senza errori
 Si deve prevedere il caso di
ridefinizione dei metodi doppi da
parte di B2
A
B1
B2
C
Ereditarietà multipla:
il problema del diamante
Soluzione Lineare:
 La gerarchia di derivazione viene linearizzata
 Si elimina l’invocazione multipla di metodi
della soluzione precedente
 Svantaggi
 Lo sviluppatore non è al corrente della
gerarchia di sottotipazione implicita
 La selezione del metodo da utilizzare è a
discrezione del compilatore
 Problemi nel collegamento con il genitore
effettivo
A
B2
B1
C
Ereditarietà multipla
Implementazione C++
C++ Ereditarietà singola
 In C++ una oggetto è una regione di memoria contigua
class A {
int a;
void f(int i);
};
int a
A* pa;
pa->f(2);
 Nell’area di memoria riservata all’oggetto pa viene solo
salvato un intero
 La funzione f, essendo non-virtual (statica), è definita
esternamente all’oggetto pa, quindi è come fosse una
funzione/procedura normale.
C++ Ereditarietà singola
 Gli oggetti composti (derivati) sono costruiti dal
compilatore concatenando le aree di memoria.
class A { int a; void f(int); };
class B : A { int b; void g(int); };
class C : B { int c; void h(int); };
int a
int b
int c
 Se la classe definisce metodi virtual entra in gioco la
tabella delle funzioni (VMT): ogni oggetto ha un
puntatore alla VMT, che permette di identificare la
funzione effettivamente da usare.
C++ Polimorfismo
 Il polimorfismo è un concetto fondamentale della
programmazione OO
 Consente che gli oggetti assumano comportamenti
differenti a seconda del contesto in cui operano
 In particolare se Ptr è un puntatore di tipo T, allora Ptr
può puntare non solo a istanze di tipo T ma anche a
istanze di classi derivate da T
T* Ptr = 0;
// Puntatore nullo
/* ... */
Ptr = new Td; // Td è una classe derivata
da T
C++ Polimorfismo (2)
 C++ fa in modo che il corretto tipo dell’oggetto venga
determinato automaticamente alla chiamata della
funzione
 In questo modo il linking della funzione viene rimandato
a runtime (binding dinamico)
 Per fare ciò bisogna dichiarare la funzione membro
virtual
class T {
public: virtual void Paint();
};
C++ Polimorfismo:
Implementazione
1. I metodi virtuali vengono ereditati allo stesso
modo di quelli non virtual, possono anch'essi essere
sottoposti a overloading ed essere ridefiniti
2. non c'e` alcuna differenza eccetto che una loro
invocazione viene risolta a run-time
3. In una classe con un metodo virtuale, il compilatore
associa alla classe (non all'istanza) una tabella
(VMT) che contiene per ogni metodo virtuale l'indirizzo
alla corrispondente funzione
4. Ogni istanza di quella classe conterrà poi al suo
interno un puntatore (VPTR) alla VMT
C++ Polimorfismo:
Overhead
 L'invocazione di un metodo virtuale e` piu` costosa di
quella per una funzione membro ordinaria, tuttavia il
compilatore puo` evitare tale overhead risolvendo a
compile-time tutte quelle situazioni in cui il tipo e`
effettivamente noto
Td Obj1;
T* Ptr = 0;
Obj1.Paint(); // Chiamata risolvibile staticamente
Ptr->Paint(); // Questa invece no
Polimorfismo
nell’ereditarietà multipla
class A { virtual void f(); };
class B { virtual void f(); virtual void g() };
class C: A, B { void f(); };
A* pa = new C;
B* pb = new C;
C* pc = new C;
C eredita sia da A che da
B, dunque l’assegnazione
è corretta
pa->f();
pb->f();
pc->f();
Tutte tre le chiamate
invocano C::f()
C++ Classi astratte
 Ereditarietà e polimorfismo possono essere combinati per
realizzare classi il cui unico scopo è creare una interfaccia
comune a una gerarchia di classi
class TShape {
virtual void Paint() = 0;
Funzioni
virtual void Erase() = 0;
virtuali pure
};
 Una classe che possiede funzioni virtuali pure è detta
classe astratta e non è possibile istanziarla
 Può essere utilizzata unicamente per derivare nuove
classi forzandole a fornire determinati metodi
C++ Ereditarietà multipla:
Ambiguità
Analizziamo il caso in cui la
classe D derivi da B1 e B2
B1
B2
B1
B2
D
class Base1 {
public:
void f();
};
class Base2 {
public:
void f();
void f2();
};
class Derived : Base1, Base2 {
// Non ridefinisce f()
};
La classe Derived eredita piu`
volte gli stessi membri, in
particolare la funzione f()
C++ Ereditarietà multipla:
Ambiguità – soluzione esplicita
Derived x; x.f() //Errore, è ambiguo!
N.B.: questo è un errore che appare solo a runtime
Soluzione: Derived x; x.B1::f()
 quanto detto vale anche per gli attributi;
 non è necessario che la stessa definizione si trovi in più
classi basi dirette, è sufficiente che essa giunga alla
classe derivata attraverso due classi basi distinte
 il problema non si sarebbe posto se Derived avesse
ridefinito la funzione membro f().
C++ Ereditarietà multipla:
Ambiguità - Implementazione

Come implementa il C++ una soluzione esplicita
attraverso qualificatore x.b1::f() ?
1. b1::f() si aspetta un puntatore b1* (che diventa il suo this)
2. A runtime conosciamo però solo il puntatore della classe
derivata “derived”
3. Il compilatore aggiunge un opportuno delta
(memorizzato nella VMT) per raggiungere la parte
relativa a B1 in “derived”
4. In pratica, il compilatore trasforma una chiamata diretta in
una indiretta, sommando un offset
C++ Ereditarietà multipla:
Implementazione
Delta(B2)
Parte B1
VMT
C::f()
0
C::f()
-delta(B2)
Parte B2
Parte D
B2::g()
0
C++ Ereditarietà multipla:
Ambiguità
 Il problema dell'ambiguità può essere portato al caso
estremo in cui una classe erediti più volte una stessa
classe base
class Base { };
class Derived1 : Base { };
class Derived2 : Base { };
class Derived3 : Derived1, Derived2 { };
C++ Ereditarietà multipla:
Ambiguità
Derived3
Derived1
Derived2
Base
Base
C++ Ereditarietà multipla:
Ambiguità – ereditarietà virtuale
 Il C++ permette di risolvere il problema molto
elegantemente con l’uso di classi base virtuali
class
class
class
class
Base { };
Derived1 : virtual Base { };
Derived2 : virtual Base { };
Derived3 : Derived1, Derived2 { };
 Quando una classe eredita tramite la keyword virtual il
compilatore non copia il contenuto della classe base
nella classe derivata, ma inserisce nella classe derivata
un puntatore ad un’unica istanza della classe base
C++ Ereditarietà multipla:
Ambiguità – ereditarietà virtuale
Derived3
Derived1
virtual
Base
Derived2
C++ Ereditarietà multipla:
Ambiguità – ridefinizione
 In alcuni casi l’ambiguità persiste. Supponiamo che una
delle classi intermedie ridefinisca una funzione membro
della classe base.
class Base {
Se Derived3 non
public: void DoSomething();
ridefinisce DoSomething
};
si crea ambiguità!
class Derived1
public: void
};
class Derived2
public: void
};
class Derived3
: virtual Base {
DoSomething();
: virtual Base {
DoSomething();
Quale metodo usare?
Il compilatore C++
segnala errore!
: Derived1, Derived2 { };
C++ Ereditarietà multipla:
Ambiguità – ridefinizione
 La situazione è diversa se solo una delle classi intermedie
fa la ridefinizione
class Base {
public: void DoSomething(); };
class Derived1 : virtual Base {
public: void DoSomething(); };
class Derived2 : virtual Base {
/* … */ };
Solo Derived1 ridefinisce
DoSomething
(definizione dominante)
Il compilatore C++ non
segnala errore!
class Derived3 : Derived1, Derived2 { };
C++ Ereditarietà multipla:
Ambiguità – Esempio
La “virtualità” di una classe non è una sua caratteristica,
ma è data dall’essere dichiarata come tale nelle classi
che la ereditano. Vediamo un esempio:
class A : virtual L {...}; In questo caso la
class B : virtual L {...}; classe C avrà un solo
riferimento ad un
class C : A , B {...};
oggetto di classe L
class D : L,C{...};
In questo caso invece la
classe D avrà due “sottooggetti” di tipo L, un virtuale
e uno normale
C++ Ereditarietà multipla:
i costruttori
 Le classi derivate normalmente chiamano
implicitamente (o esplicitamente) i costruttori delle
classi base
 In caso di ereditarietà multipla con classe base virtuale
si pone il problema di decidere chi inizializza la classe
base (in quale ordine)
 In C++ le classi virtual sono inizializzate dalle classi
massimamente derivate
 In generale i costruttori sono eseguiti nell’ordine in cui
compaiono nella dichiarazione eccetto quelli delle classi
virtual, eseguiti prima
C++ Ereditarietà multipla:
i distruttori
 Stesso discorso per i distruttori, ma in ordine
contrario
 Il compilatore C++ si preoccupa di
distruggere le classi virtual una sola volta,
anche se vengono ereditate molteplici volte
C++ Ereditarietà multipla:
Problemi di efficienza
L’ereditarietà multipla comporta alcuni costi in termini di
efficienza:
1.Sottrazione di una costante per ogni accesso ai membri
delle classi base
2.Un word per funzione in ogni VMT (per il delta)
3.Un riferimento in memoria ed una sottrazione per ogni
chiamata a funzione virtuale
4.Un riferimento in memoria ed una sottrazione per ogni
accesso ai membri di una classe base virtuale
La 1 e la 4 sono penalizzanti solo se l’ereditarietà multipla
è effettivamente usata. La 2 e la 3 sempre
C++ Ereditarietà multipla:
Considerazioni
 Il metodo qui presentato offre due modalità di
estendere il “name space” di una classe:


classe base
classe base virtuale
 Comunque, le regole per gestire questi due tipi di
classi sono indipendenti dal tipo di classe
effettivamente usata, inoltre:



le ambiguità sono illegali
le regole per la gestione dei vari membri sono le stesse
che con ereditarietà singola
le regole di visibilità ed inizializzazione sono le stesse
dell’ereditarietà singola
 Violazioni di queste regole sono segnalate a compiletime
C++ Ereditarietà multipla:
Conclusioni
 L’ereditarietà multipla, in una forma pratica da usare,
è relativamente semplice da aggiungere al C++
 Per essere implementata richiede piccolissime
modifiche alla sintassi e si adatta naturalmente alla
già preesistente struttura
 L’implementazione è efficiente sia in tempo che in
spazio, dal momento che, soprattutto su calcolatori
moderni, semplici operazioni di somma o sottrazione
o un campo in più nella VMT non costituiscono un
overhead pesante
 La compatibilità con il C non è compromessa, e
neppure la portabilità
Ereditarietà multipla
Implementazione modello
“Twin Objects”
(J.Templ)
Twin Objects
 Sono un modo di realizzare l’ereditarietà multipla
usando l’ereditarietà singola
 Possono essere usati per implementare l’ereditarietà
multipla in linguaggi che non la supportano, ad
esempio in Java
 Aiutano a risolvere problemi tipici dell’ereditarietà
multipla quali l’ambiguità dei nomi
Twin Objects - modello
Multiple Inheritance
B
A
Twin objects
B
A
C
C
CA
T1
T2
CB
 CA e CB sono chiamati twins (gemelli)
 Sono sempre generati assieme e legati dai
puntatori T1 e T2
Twin Objects - modello
 Se la classe C eredita da n classi base, ci saranno n
twins da gestire
A
B
CA
C
E
CE
CB
Twin Objects - modello
Se dobbiamo inserire nella nostra classe attributi o
metodi aggiuntivi (non definiti nelle classi basi) abbiamo
due metodi:
 creiamo una classe
aggiuntiva C in cui li
inseriamo
 li mettiamo in uno dei due
twin (ad es. in C1, che
chiamiamo C); questo è
l’approccio seguito
normalmente
P2
P1
C
T1
T2
C2
Twin Objects - ereditarietà
P2
P1
C
D
T1
T2
C2
No
D non eredita da C2!
P2
P1
C
T1
T2
C2
D1
T1
T2
D2
Twin Objects Collaborazione
 Ogni classe figlio è responsabile per la comunicazione
con il suo padre e si occupa di inoltrare i messaggi alle
classi gemelle
 I client referenziano uno dei figli direttamente, e tutti gli
altri tramite i puntatori a twin (la ‘T’ negli esempi)
 I client che necessitano di comunicare con uno dei
Padri, lo fanno attraverso la rispettiva classe-figlio
Twin Objects Implemenazione
 Astrazione: le classi gemelle devono cooperare
strettamente tra loro, permettendo di accedere ai loro
membri privati (visibilità package in Java). Il tutto deve
apparire come un unico oggetto dall’esterno.
 Efficienza: l’uso di twin objects sostituisce le relazioni
per ereditarietà con relazioni per composizione. Ciò
comporta la necessità di inoltrare messaggi e quindi
minore efficienza, ma poiché l’ereditarietà multipla è in
genere più lenta, non si notano differenze sostanziali.
Twin Objects Ottimizzazioni
 Raggruppare i Twin in un unico blocco contiguo per
velocizzare l’allocazione dell’oggetto che li
usanecessità di utilizzare puntatori nel blocco per
collegare i twin
 sostituire il puntatore con un offset relativo all’inizio
del twin
 se l’allocazione degli oggetti client e dei twin è resa
uguale per ogni istanza, l’offset è una costante
memorizzabile a parte
 le VMT possono essere memorizzate
contiguamente in un blocco
Twin Objects - Esempio
 Java non consente l’ereditarietà multipla, tuttavia in
alcuni casi serve poter mettere assieme oggetti di
natura diversa, ad esempio implementando applet che
reagiscono alle azioni del mouse
 Costruendo un applet, serve poter ereditare da un
generico Applet a cui verrà ridefinito il metodo .Paint()
e da una classe StdMouseListener di cui verranno
ridefiniti i metodi mousePressed(), mouseClicked() e
mouseReleased()
 Lo schema che segue riassume la struttura del nostro
oggetto composto MyApplet + MyAppletListener
Twin Objects – Esempio
StdMouseListener
Applet
resize()
paint()
…
mousePressed()
mouseClicked()
mouseReleased()
MyApplet
C
paint()
MyAppletListener
T1
T2
mousePressed()
mouseClicked()
mouseReleased()
Twin Objects - Esempio
class Applet {
public void paint();
public void resize();
…
}
class StdMouseListener {
public void mousePressed();
public void mouseClicked();
public void mouseReleased();
…
}
 Queste sono le definizioni delle due classi base di cui desideriamo
fare ereditarietà multipla attraverso l’uso del modello Twin Objects
Twin Objects - Esempio
class MyApplet extends Applet {
MyAppletListener listener; /* il Twin */
public void paint() { /* ridefinisco */ }
…
}
class MyAppletListener extends StdMouseListener {
MyApplet applet; /* il Twin */
public void mousePressed () { /* ridefinisco */ }
public void mouseClicked () { /* ridefinisco */ }
public void mouseReleased () { /* ridefinisco */ }
…
}
 Ogni “twin” eredita il proprio parent ridefinendone i metodi
opportuni e si occupa di comunicare con il proprio fratello.
Twin Objects - Esempio
 Layout in memoria:
Applet
MyApplet
StdMouseListener
twins
MyStdMouseListener
 Come evidente, si tratta di oggetti completamente
separati a livello di memoria. Il link tra le classi è fatto a
livello di applicazione.
C++ - Contro esempio


class Applet {
virtual void paint();
virtual void resize();
…
}
class StdMouseListener {
virtual void mousePressed();
virtual void mouseClicked();
virtual void mouseReleased();
…
}
Come per l’esempio Twin Objects, abbiamo due classi base iniziali
da cui vogliamo fare ereditarietà multipla...
“virtual” indica che le funzioni sono di tipo latebinding (come
nell’es. Java) e non statiche.
C++ - Contro esempio
class MyApplet : Applet, StdMouseListener {
void paint() { /* ridefinisco */ }
void mousePressed() { /* ridefinisco */ }
void mouseClicked() { /* ridefinisco */ }
void mouseReleased() { /* ridefinisco */ }
…
}
 Il C++ ci consente di avere un’unica classe MyApplett che
eredita contemporaneamente da Applet e StdMouseListener.


Ridefinisco i metodi secondo le esigenze; per fortuna non ci sono
name clashes quindi non ci preoccupiamo di usare qualificatori
espliciti
A livello implementativo, il compilatore traduce una chiamata del
tipo *obj->paint() in una chiamata indiretta sommando un delta
riferito alla classe parent e memorizzato nella VMT
C++ contro esempio
VMT
delta(StdML)
Parte
Applet
Parte
Stdmouse
listener
Parte
Myapplet
Myapp::paint()
Myapp::paint()
0
-delta(StdML)
metodi ridefiniti da
MyApplet
Considerazioni di Templ
sull’ereditarietà multipla
 L’implementazione dell’ereditarietà multipla del
C++ porta ad un overhead anche
dell’ereditarietà singola
 Il codice deve essere riaggiustato per cambiare
il “self”
 L’ereditarietà multipla in se stessa non è né
molto utile né veramente necessaria dal
momento che può essere facilmente simulata
 Il vantaggio che offre è puramente sintattico
 Il suo costo non è giustificato dalle opportunità
che offre
Conclusioni
 L’ereditarietà multipla è uno strumento potente che
consente di affrontare problemi complessi con
eleganza
 La sua implementazione nativa può generare un lieve
decadimento del performances anche quando non
viene usata (vedi C++)
 Gestire un linguaggio con ereditarietà multipla può
divenire comunque complesso e poco chiaro (sia per
il programmatore che per l’implementatore)
 I linguaggi moderni tendono ad evitarla (es. Java),
adoperando tecniche altrettanto efficaci come i Twin
Objects, senza overhead e complicazioni, dal
momento che i vantaggi, sebbene ci siano, forse non
giustificano le necessarie modifiche dei linguaggi OO
già esistenti
Riferimenti
 “Twin – A Design Pattern for Modeling Multiple
Inheritance” J. Templ
 “Multiple Inheritance for C++” Bjarne Stroustrup
 Manuale del C++ Bjarne Stroustrup
Scarica

Ereditarietà Multipla