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