05-RelazioniTraClassiEdEreditarieta
Transcript
05-RelazioniTraClassiEdEreditarieta
Relazioni tra classi ed ereditarietà Michelangelo Diligenti Ingegneria Informatica e dell'Informazione [email protected] Argomenti ● Relazioni tra classi Unified Modeling Language (UML) Diversi tipi di relazione Ereditarietà e funzioni virtuali RTTI Unified Modeling Language (UML) ● ● Modello grafico per la modellazione delle classi e le loro relazioni La modellazione di una classe Dati e metodi listati uno per riga. La riga parte con Nome classe Dati Metodi • + per membri e metodi pubblici • - per membri e metodi privati • # per membri e metodi protected (vedremo cosa sono) UML esempio singola classe Persona -nome: string -altezza: int -peso: int +posizione: float Suddivisione metodi per Tipo di funzione svolta Metodo specifica visibilità, suo nome, parameteri passati e valore ritornato <<constructor>> +Person( ) +Person(name:string) <<process>> +Apprende( ):void +PrendeAutobus(numero:int):void <<helper>> -PranzoLungo( ):bool Relazioni tra classi ● Supponiamo di dover modellare una macchina ● Dobbiamo modellare le sue sottoparti ● Ma anche Pedestrian, Strada, ecc. E data una macchina ci sono tanti tipi ● Classe Engine, Wheel, ecc. Poi vi sono classi separate ma correlate ● Classe Car SUV, utilitaria, ecc. O tante marche FIAT, Ferrari, BMW Relazioni tra classi ● Modellare dele sue sottoparti ● Poi vi sono classi separate ma correlate ● Composizione Namespaces Sottotipi (is-a relation) Ereditarietà, la più importante! Composizione ● Supponiamo di dover modellare un aeroplano Un aeroplano è formato da sottoparti: ali, coda, fusoliera, cabina, ecc. Ogni parte può essere complessa ed essere formata da altre sottoparti Quando si compone un oggetto mettendo insieme le sue sottoparti si parla di composizione Composizione: C++ ed UML class Aeroplano { Aeroplano private: Fusoliera fusoliera; Coda coda; Ali ali; … }; Fusoliera Coda ... Composizione in C++ ● Costruttori e inizializzazione delle sottoparti Se l'utente non specifica nulla, il default costructor viene chiamato automaticamente class Aeroplano { private: Fusoliera fusoliera; Coda coda; Ali ali; public: Aeroplano() { } }; Nessuna specifica, fusoliera e coda sono inizializzati con il proprio costruttore di default Composizione in C++: costruttori ● Se si desidera un costruttore diverso, usare costruttori pre-run Sottoparti inizializzate prima esecuzione del costruttore dell'oggetto composto La sintassi del costruttore è la seguente NomeClasse([params]) : subpart1(params), subpart2(params), ... { codice costruttore } ATTENZIONE: Ordine delle sottoparti deve seguire l'ordine nella dichiarazione Composizione in C++: costruttori ● Esempio Chiama costruttore di default per ali, class Aeroplano { fusoliera e coda inizializzate con un private: costruttore che prende int in input Fusoliera fusoliera; Coda coda; Ali ali; fusoliera e coda inizializzate public: con un costruttore che prende int in input, ali con costruttore Aeroplano() : fusoliera(3), coda(2) { } che prende due int Aeroplano(int flen, int clen, int alen) : fusoliera(flen), coda(clen), ali(2, alen) { } Aeroplano(int len) : ali(2, len), coda(2) { } }; NO! Errore in compilazione ali non può precedere coda (dichiarata prima) Composizione in C++: costruttori ● Esempio Anche tipi base possono essere inizializzati class Aeroplano { in questo modo: chiamando i loro costruttori private: Fusoliera fusoliera; Coda coda; float carburante; int num_motori; public: Aeroplano() : fusoliera(3), coda(2), carburante(100.0), num_motori(2) { } Aeroplano(const int fusoliera_lunghezza, const int num_motori_) : fusoliera(fusoliera_lunghezza), coda(2), carburante(100.0), num_motori(num_motori_) { } }; Lo stesso meccanismo vale anche per costruttori non di default Composizione in C++: costruttori ● Possibile anche usare copy constructors in inizializzazione – Esempio class Aeroplano { private: Fusoliera fusoliera; Coda coda; Ali ali; public: ... Fusoliera e coda inizializzate con copy costrustor. Aeroplano(const Fusoliera& fus, const Coda& cod) : fusoliera(fus), coda(cod) { } }; Composizione in C++: costruttori ● Con allocazione dinamica delle sottoparti – Usare se alcune parti possono rimane indefinite class Aeroplano { Alcuni oggetti sono puntatori, non nessariamente tutti inizializzati dai costruttori. private: Fusoliera* fusoliera; Esempio qui li lasciamo NULL, utile quando Coda* coda; si vuole lasciare alcune parti non definite Ali ali; (lazy initialization) public: Aeroplano() : fusoliera(NULL), coda(NULL) { } Aeroplano(int flen, int clen, int alen) : coda(NULL), ali(2, alen) { fusoliera = new Fusoliera(flen); if (clen > 0) coda = new Coda(clen); Gli oggetti allocati dinamicamente sono } }; esplicitamente costruiti chiamando un costruttore via new Composizione in C++: distruttori ● Quando un oggetto viene distrutto Si esegue automaticamente il distruttore delle classi usate in composizione Esempio: se una classe aeroplano contiene un istanza di classe Coda ed Ali Viene invocato il distruttore per i dati membri: ~Coda() per la coda e ~Ali() per le ali Questo avviene automaticamente se Coda ed Ali non sono stati allocati dinamicamente cioe' come sulle slide viste in precedenza se allocati dinamicamente sta a voi gestire la distruzione Composizione in C++: distruttori ● Con allocazione dinamica Distruzione distrugge i puntatori e non i valori puntati. class Aeroplano { private: Deallocazione adesso non avviene automaticamente Fusoliera* fusoliera; tramite le chiamate automatiche dei distruttori (per Coda* coda; gli oggetti allocati dinamicamente) Ali ali; public: La deallocazione la dovete fare voi ... ~Aeroplano() { if (coda != NULL) delete coda; if (fusoliera != NULL) delete fusoliera; } }; In generale questo meccanismo vi complica le cose. Usarlo solo se vi sono delle sotto-parti che potrebbero non servire e si vuole rimandare la loro inizializzazione Ereditarietà ● Supponiamo di aver modellato il concetto di Aeroplano Adesso ci viene chiesto di modellare un aliante, un jet, od un aeroplano ad elica Non ha senso ripartire da zero. L'aeroplano già modella dati comuni Lunghezza fusoliera Lunghezza ali Numero passeggeri Peso a pieno carico ecc. Ereditarietà ● OO risolve questo problema definendo Classe base (o classe padre o superclass): un aeroplano con dati e metodi comuni Classe Aliante che eredita tutte le caratteristiche (dati e metodi) di Aeroplano ed eventualmente ne aggiunge altri (come vento_minimo) Classe Jet che aggiunge un attributo di tipo MotoreJet Classe AeroplanoElica che aggiunge attributo di tipo MotoreElica Ereditarietà ● Si estende una classe in un'altra più definita Si parla di Ereditarietà Permette di condividere similarità Si riusa il codice! Si modellano solo le differenze La classe base viene detta superclasse o classe padre o classe base (base class, parent class, superclass) Classe derivata viene detta sottoclasse o classe derivata o classe figlia (subclass, child class, derived class) Ereditarietà ● ● Dati e metodi della classe base sono ereditati dalla figlia Istanza della sottoclasse è istanza anche della classe padre ● La sottoclasse però aggiunge dati e/o metodi nuovi ● ATTENZIONE: Costruttori e distruttori non sono ereditati, tranne quelli di default Vanno ridefiniti se necessario Ereditarietà e UML UML rappresenta l'ereditarieta come una freccia dalla classe derivata e quella padre Esempio, modellazione di forme geometriche Forma Ellisse Cerchio Poligono Quadrato Rombo Esagono ... Specializzazione Generalizzazione ● Ereditarietà in C++ ● In C++ si segnala che una classe eredita da un'altra usando la sintassi: class ClasseFiglia : public ClassePadre { … }; ● Esempio class Poligono { … }; class Rettangolo : public Poligono { … }; Ereditarietà: aggiunta dati e metodi ● Classe figlia spesso aggiunge dati membri o metodi class Forma {… }; class Cerchio : public Forma { private: float raggio; public: float Raggio() const; }; Forma Cerchio +raggio: float +Raggio(): float Ereditarietà e C++ class Forma { ... private: int area; int perimetro; public: Forma() { cout << sizeof(*this); } }; class Cerchio : public Forma { private: float raggio; public: Cerchio() { cout << sizeof(*this); } }; area perimentro raggio Forma Cerchio Nuovi dati sono aggiunti in coda sizeof(Cerchio) > sizeof(Forma) Ereditarietà e costruttori in C++ ● Costruttore della classe padre è invocato nel costruttore con sintassi simile a composizione NomeClasse([params]) : NomePadre([params]), subpart1(params), … { } Composizione ed ereditarietà convivono Esempio class Poligono { class Rettangolo : public Poligono { private: public: int n_lati; public: Poligono(int n) { n_lati = n; } }; Rettangolo() : Poligono(4) {} }; Ereditarietà e costruttori in C++ ● Se non specificato diversamente, costruttori di default sono invocati Però devono essere definiti, se non lo sono il compilatore emette un errore Ordine chiamate da classe più generale a più specifica Forma Ellisse Cerchio Esempio data la struttura di classi in figura, si chiama: 1) Forma() 2) Ellisse() 3) Cerchio() Ereditarietà e costruttori in C++ ● Tuttavia possibile specificare esplicitamente i costruttori da chiamare del padre Obbligatorio se quelli di default non sono definiti Si riusa la stessa sintassi per l'inizializzazione di dati membri Forma Ellisse Cerchio Ereditarietà e costruttori in C++ ● Esempio 2: Forma Ellisse Cerchio class Forma { private: float area; public: Forma() : area(0.0) { } Forma(const float area_) : area(area_) { } }; class Ellisse : public Forma { float fuoco1; float fuoco2; public: Ellisse() : fuoco1(0.0), fuoco2(0.0) { } Ellisse(const float fuoco1_, const float fuoco2_, const float area_) : Forma(area_), fuoco1(fuoco1_), fuoco2(fuoco2_) { } }; class Cerchio : public Ellisse { public: Cerchio() { } Cerchio(const float centro_, const float area_) : Ellisse(centro_, centro_, area_) { } }; Ereditarietà in C++: distruttori ● Quando il distruttore per una classe viene invocato Distruttore stesso viene eseguito Si esegue il distruttore della classe padre Ricorsivamente fino esecuzione distruttore della radice Forma Ellisse Cerchio Distruttori invocati dallo specifico al generale risalendo l'albero delle relazioni Data la struttura di classi in figura, si chiama 1) ~Cerchio() 2) ~Ellisse() 3) ~Forma() Ereditarietà in C++: protected ● Oltre public e private vi è un altro livello di visibilità in C++ per membri e metodi: protected Indica membro o metodo visibile solo a sottoclassi Per il resto del mondo è non visibile come un private class Ellisse : public Forma { protected: float fuoco1; float fuoco2; public: ... }; Questi membri sono visibili in classi derivate ma non per il resto del mondo. Quanto si usa ereditarietà si tende ad usare più protected che private Ereditarietà ed overriding ● Si parla di overriding di metodi quando Classe figlia ridefinisce metodo della classe padre Con stessi argomenti Diverso da overloading visto in precedenza Overloading è nella stessa classe Metodi hanno diversi argomenti Forma Cerchio +raggio: float +Disegna() +Move(int) +Move(int, int) Disegna() Disegna() è overridden Move() overloaded Overriding e metodi del padre ● Se necessario aggiungere codice a metodo del padre Non necessario ridefinirlo nel figlio Possibile invocarlo con sintassi NomePadre::MetodoDaChiamare([params]) class Persona { private: string nome; string luogonascita; public: string ToString() const { return nome + “\t” + luogonascita; } }; class Studente : public Persona { private: string facolta; string corso; public: string ToString() const { return Persona::ToString() + “\t” + facolta + “\t” + corso; } }; Esercizio ● ● Creare una gerarchia di classi per gestire le persone che lavorano o studiano all'università di Siena Persone sono studenti o ricercatori o docenti o ammistratori Tutti hanno nome, luogo di nascita, indirizzo di lavoro Studenti hanno inoltre facoltà e corso che frequentano Ricercatori l'argomenti di ricerca Docenti i corsi che insegnano Amministrativi l'ufficio di appartenenza Definire la gerarchia di classi, i dati membri, i metodi accessors e costruttori Ereditarietà e polimorfismo ● Esempio di ereditarietà ed overriding class Poligono {… void Disegna(); }; ● class Rettangolo : public Poligono { void Disegna(); }; PROBLEMA: se un'istanza della classe figlia è anche istanza della classe padre quale metodo chiamo nei seguenti casi? Poligono p; p.Disegna(); Rettangolo p; p.Disegna(); Poligono* p = new Rettangolo(); p->Disegna(); Secondo voi questi 3 casi sono sintatticamente corretti? Notate il casting qui Ereditarietà e polimorfismo ● ● Obiettivo è sempre chiamare il metodo più specifico PROBLEMA: il compilatore non può spesso sapere qual'è la classe più specifica Poligono* p = NULL; if (string(argv[1]) == “rettangolo”) p = new Rettangolo(); Si, il casting è corretto, Un Rettangolo è anche un Poligono else if (string(argv[1]) == “poligono”) p = new Poligono(); p->Disegna(); ● La classe di p è definita a run-time! Sarà un poligono ma forse anche un poligono o ... Ereditarietà e polimorfismo ● OO impone una diversità fondamentale: ● Si decide il metodo a run-time, quando l'oggetto è disponibile ● il compilatore non decide quale metodo chiamare nel momento della compilazione run-time, or dynamic binding Polimorfismo: 2 oggetti di tipo Poligono rispondono diversamente allo stesso input (uno è anche Rettangolo e l'altro no) Vedremo come realizzarlo in C++, come scritto nella pagina precedente non funziona! Ereditarietà e C++: funzioni virtuali ● Meccanismo polimorfico di late-binding non è default in C++ Si realizza solo per i metodi virtuali Esempio precedente class Poligono {… void Disegna(); }; class Rettangolo : public Poligono { void Disegna(); }; Poligono* p = new Rettangolo(); p->Disegna(); Qui si chiamerebbe Poligono::Disegna() Ereditarietà e C++: funzioni virtuali ● Definire un metodo virtuale richiede che sia specificata la keyword virtual Esempio precedente class Poligono { class Rettangolo : public Poligono { int n_lati; float dim_lato; virtual void Disegna(); virtual void Disegna(); virtual ~Poligono() { } virtual ~Rettangolo() { } }; }; Poligono* p = new Rettangolo(); p->Disegna(); Se ci sono metodi virtuali è obbligatorio definire un distruttore virtuale Qui si chiama Rettangolo::Disegna() Ereditarietà e C++: funzioni virtuali ● In C++ il polimorfismo, meccanismo alla base dell'OO è opzionale! Sembra strano Causa di molti bugs, facile scordarsi di ”virtual” Perché lo standard del C++ ha deciso questo? Per capirlo dobbiamo studiare i dettagli di come il late-binding viene realizzato in C++ Vtables ● Poiché il binding avviene a run-time Ogni oggetto ha tabella di puntatori ai suoi metodi Detta vtable n_lati ... Vtable Poligono::Disegna() Poligono n_lati dim_lato Rettangolo Vtable Rettangolo::Disegna() Vtables ● Le vtables costano Memoria: devo memorizzare un puntatore per metodo puntato Tempo: ogni volta che un metodo è invocato, devo 1) cercare la vtable 2) dereferenziare il puntatore 3) chiamare la funzione come sempre Questo costa molto di più che fare solo 3) Funzioni virtuali non possono essere inline Vtables ● C++ vuole mantenere l'efficienza potenziale del C ● Se non si usano metodi virtuali Pertanto il meccanismo del late-binding è opzionale in C++ Linguaggi puri OO lo hanno come default Scelta molto controversa Causa di bugs La perdita di velocità è spesso trascurabile Metodi e classi astratte ● ● Talvolta una classe padre non può definire un metodo perché non abbastanza definita Tuttavia tutti gli oggetti che deriveranno dalla classe devono avere una certa funzionalità Classe padre definisce l'interfaccia Forza ogni classe derivata ad implementare la funzionalità Metodo definito ma non implementato viene detto astratto Classe con metodi astratti si dice astratta a sua volta Metodi e classi astratte ● Non è possibile generare un oggetto di classe astratta Non sapremmo come gestirlo: i metodi astratti non sono definiti In C++ è però possibile definire una classe figlia non astratta, e fare il casting al padre di classe astratto Metodi da chiamare sono definiti dalla vtable dell'istanza In C++ le classi astratte sono maneggiabili solo per puntatore Metodi astratti in C++ ● Si realizzano con la sintassi virtual tipo NomeMetodo([args]) = 0; Esempio Forma non ha informazioni per disegnare l'istanza ma ogni class Forma {... oggetto che deriva da Forma virtual void Disegna() = 0; deve poter essere disegnato }; class Rettangolo : public Forma { virtual void Disegna() {...} Ogni oggetto derivato }; da Forma deve implementare class Cerchio : public Forma { Disegna se vuole poter essere virtual void Disegna() {...} utilizzabile }; Metodi astratti in C++ ● Non si può costruire ed usare un oggetto istanza solo della classe con metodi astratti Forma f; Solo la classe figlia che li implementa può essere costruita // NO! Forma ha metodi astratti Rettangolo r; // OK, Disegna è implementato Forma* f = new Rettangolo(); // OK, f è Rettangolo, Disegna() trovato via vtable Ecco perché i metodi astratti devono essere virtuali, per poter chiamare i metodi implementati partendo da un oggetto di classe padre Metodi astratti in C++: esempio class Forma { virtual float Area() = 0; virtual void Disegna() = 0; }; class Rettangolo : public Forma { virtual float Area() { … /* implementazione */ } virtual void Disegna() { … /* implementazione */ } }; class Cerchio : public Forma { virtual float Area() { … /* implementazione */ } virtual void Disegna() { … /* implementazione */ } }; Metodi astratti in C++: esempio // Costruzione specifica sceglie una sottoclasse Forma* f = NULL; if (string(argv[1]) == “rettangolo”) f = new Rettangolo(); else if (string(argv[1]) == “cerchio”) f = new Cerchio(); // Implementazione comune a tutti! cout << “Area “ << f->Area(); f->Disegna(); if (f != NULL) delete f; Classi pure astratte (pure abstract) ● Possibile definire classi con solo metodi astratti Dette classi pure astratte Utili per definire interfacce Delegano totalmente l'implementazione alle classi figlie Esempio (come classe Forma precedente) class DataCompressor { public: virtual void Compress(const string& in_data, string* out_data) = 0; virtual void Decompress(const string& in_data, string* out_data) = 0; }; Ereditarietà e casting ● Upcasting passa da figlio a padre operazione sicura e sempre accettabile istanza di classe figlia è sempre istanza del padre class Parent { } class Child : public Parent { } Child c; Parent* p = &c; Ereditarietà e casting ● Upcasting operazione fondamentale in C++ ● Pattern di programmazione tipico è il seguente Si crea un istanza di classe figlia chiamando un costruttore specifico Upcasting su puntatore di classe padre Uso dell'istanza tramite l'interfaccia pubblica virtuale Max flessibilità, cambiare istanza non tocca il codice Si deve solo chiamare un costruttore diverso Tutto il resto del codice rimane inalterato Abbiamo riutilizzato il codice! Ereditarietà e casting ● Esempio del meccanismo class Forma { virtual void Print() = 0; }; class Rettangolo : public Forma { virtual void Print() { cout << ”Rettangolo\n”;} Usare un cerchio invece }; che rettangolo cambia class Cerchio : public Forma { una sola riga virtual void Print() { cout << ”Cerchio\n”;} }; int main() { Creo oggetto figlio e Forma* f = new Rettangolo(); faccio upcasting f->Print(); delete f; Uso interfaccia pubblica e virtuale } Ereditarietà e casting ● Downcasting passa da padre a figlio operazione pericolosa padre ha più figli e si potrebbe scegliere il figlio sbagliato class Parent { } class Child1 : public Parent { } class Child2 : public Parent { } Parent* p = new Child1; Child1* c1 = (Child1*)p; // OK, p era un Child1 Child2* c2 = (Child2*)p; // No, c non è Child2, bug probabile! Esercizio ● ● ● Creare una gerarchia di classi per gestire le persone che lavorano o studiano all'università di Siena Persone sono o studenti o ricercatori o docenti o ammistratori Tutti hanno nome, luogo di nascita, indirizzo di lavoro Studenti hanno inoltre una facoltà e corso di laurea che frequentano Ricercatori hanno degli argomenti di ricerca Docenti hanno dei corsi che insegnano Amministrativi hanno un ufficio di appartenenza Deve essere possibile scrivere o leggere ogni persona su/da una stringa che ne codifica le sue caratteristiche Nel main, chiedere all'utente se vuole generare uno studente, un ricercatore un docente o un amministrativo da linea di comando, stampare su stringa le sue caratteristiche C++ e casting ● Casting comune sorgente di bugs Tipicamene sono bug difficili da tracciare ● C ha solo un tipo di cast ● C++ permette di fare cast in molti modi ● static_cast reinterpret_cast const_cast dynamic_cast Sintassi xxx_cast<tipo>(variabile) Effettua il cast della variabile al tipo specificato C++: casting e bugs class CDummy { float i,j; public: Cdummy(const float i_, const float j_) : i(i_), j(j_) { } }; class CAddition { int x,y; Casting trasforma Cdummy public: CAddition (int a, int b) { x=a; y=b; } in Caddition reinterpretando i bytes. Ma va bene? int result() { return x+y;} }; Se sizeof(CAddition) > sizeof(CDummy) int main () { ci potrebbe essere un fault! CDummy d(1.1, 2.2); CAddition* padd = (CAddition*)&d; Altrimenti il comportamento è cout << padd->result() << endl; semplicemente scorretto: leggo due float return 0; trasformando due interi bit a bit! } C++: static_cast ● static_cast equivalente al cast del C Efficiente non effettua alcun controllo di correttezza Converte tipi se il casting è noto, esempio: int i = 3; float f = (float)i; float f1 = static_cast<float>(i); Oppure per convertire tipi che sono compatibili,esempio: class CBase {}; class CDerived: public CBase {}; CDerived* a = new CDerived; CBase* b = static_cast<CBase*>(a); C++: static_cast ● Vantaggi di static_cast contro casting del C Cast esplicito e visibile si evita di non notare il cast Possibile cercare in editor dove si fa casting per scovare i bug (difficile con cast del C) Efficienza analoga al cast C C++: static_cast ● Casting del C e static_cast sono pericolosi con classi Esempio un down_cast class CBase {}; class CDerived: public CBase {}; CBase* a = new CBase; CDerived* b = static_cast<CDerived*>(a); Codice compila Ma si potrebbe avrebbe un segmentation fault od un comportamento indefinito C++: reinterpret_cast ● reinterpret_cast: cast di basso livello Per cast non ben definiti, sono cast pericolosi Il compilatore reinterpreta bit a bit i dati di un tipo in un altro Nessuna garanzia di portabilità Non dovrebbe quasi mai servire di usare cast di questo genere, ma se proprio vi serve... Esempio char c[10]; int* i = reinterpret_cast<int*>(c); i[0] = 3; C++: const_cast ● Permette di togliere constness da un tipo Esempio void Print (char* str) { std::cout << str << std::endl; } int main () { const char* c = "sample text"; Print(const_cast<char*>(c)); return 0; } In questo caso la cosa giusta sarebbe cambiare void print (char * str) → void print (const char * str) Talvolta questo non si può sempre fare Print potrebbe essere in libreria esterna (legacy code) che usiamo ma non ci appartiene const_cast ci permette di togliere const senza alterare la libreria C++: dynamic_cast ● Cast sicuro anche con classi Effettua controlli di compatibilità tra i tipi prima di effettuare la conversione Tuttavia costoso dal punto di vista delle performance rispetto a cast senza controlli Usare solo per cast insicuri in presenza di ereditarietà Per questo il C++ tiene altri tipi di cast che hanno minori penalità in termini di efficienza Di nuovo si vede come il C++ cerchi un compromesso tra pulizia dell'implementazione OO ed efficienza Vediamone i concetti fondamentali iniziando da RTTI RTTI ● Run Time Type Identification (RTTI) Permette di identificare un oggetto a run-time in modo sicuro Permette di controllare il tipo di un oggetto facendo il down casting in modo sicuro Se il down cast fallisce assegna un puntatore NULL il programmatore sa che il casting non è andato a buon fine RTTI e dynamic_cast ● RTTI si basa sul casting dynamic_cast Esempio class Forma { }; class Rettangolo : public Forma { }; class Cerchio : public Forma { }; int main() { Forma* f = new Cerchio; if (dynamic_cast<Rettangolo*>(f)) std::cout << ”Rettangolo\n”; if (dynamic_cast<Cerchio*>(f)) std::cout << ”Cerchio\n”; delete f; return 0; } RTTI e problemi ● Non abusare di RTTI, se avete molte classi si crea catena di if/else Si deve modificare il codice per ogni classe aggiunta Meglio usare l'interfaccia della superclasse Esempio equivalente al precedente ma scalabile class Forma { virtual void Print() const = 0; }; class Rettangolo : public Forma { virtual void Print() const { cout << ”Rettangolo\n”; } }; class Cerchio : public Forma { virtual void Print() const { cout << ”Cerchio\n”; } }; int main() { Chiamo metodo virtuale Cerchio c; invece che usare RTTI che Forma* f = &c; f->Print(); se ho tante classi diviene return 1; pesante } RTTI: typeinfo ● Usando la classe typeid, è possibile accedere ad informazioni di tipo che il C++ ha per ogni variabile #include <typeinfo> class CBase { virtual void f(){} }; class CDerived : public CBase {}; int main () { CBase* a = new CBase; CBase* b = new CDerived; std::cout << "a is: " << typeid(a).name() << '\n'; std::cout << "*a is: " << typeid(*a).name() << '\n'; if (typeid(*a) != typeid(*b)) std::cout << "a and b are of different types:\n"; std::cout << "a is: " << typeid(a).name() << '\n'; std::cout << "b is: " << typeid(b).name() << '\n'; return 0; } Ereditarietà multipla ● C++ supporta ereditarietà multipla Una classe può ereditare da due padri Esempio Un oggetto Child è sia istanza di Parent1 che Parent2 class Parent1 { } class Parent2 { } class Child : public Parent1, public Parent2 { } Caratteristica pericolosa che crea confusione Classi padre possono avere metodi o dati con lo stesso nome! Quale chiamo? Polimorfismo non ben definito Usare tale caratteristica solo in casi eccezionali Java, nato dopo, non supporta ereditarietà multipla Se necessaria, spesso sintomo di design errato