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 usanecessità 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