Esercizio - Ereditarietà, is-a e has-a, slicing (primo appello 2022-23, teoria)
In questa pagina 4
Domanda aperta 1 del primo appello 2022-23 del corso di Laboratorio di Programmazione, Ing. Informatica UniPD. Teoria: C++ - ereditarietà, funzioni virtuali e layout degli oggettiL'ereditarietà pubblica (class D : public B) esprime la relazione "is a": D eredita da B i membri public e protected e li estende; la composizione esprime invece "has a". Una classe base di una gerarchia disabilita copia e assegnamento (=delete) perché copiando una derivata in una base si ha lo slicing. Una funzione virtuale può essere ridefinita nelle derivate (override: stesso nome, tipi e constness) e la funzione chiamata si decide a run-time in base al tipo dell'oggetto, non del puntatore o del riferimento; override esplicito con la keyword override; una funzione virtuale pura (= 0) obbliga le derivate a ridefinirla e rende astratta la classe; il distruttore di una classe base polimorfa è virtuale. Il meccanismo è realizzato con una vtbl per classe (tabella di puntatori alle funzioni) e un vptr in ogni oggetto; i dati della derivata seguono quelli della base.C++ - ereditarietà, funzioni virtuali e layout degli oggetti →, C++ - polimorfismoPolimorfismo = abilità di associare comportamenti diversi a un'unica notazione generica. In C++ ci sono due forme principali: dinamico (ereditarietà più funzioni virtuali: vincolato dall'interfaccia della classe base, binding a run-time, permette collezioni eterogenee) e statico (template: svincolato, basta che i tipi abbiano la stessa sintassi, binding a tempo di compilazione, nessun costo a run-time ma una funzione per ogni tipo); il polimorfismo statico è lo strumento della programmazione generica. Anche l'overloading di funzioni e di operatori può essere visto come polimorfismo. Nella gerarchia si naviga con l'upcasting (implicito, sempre lecito) e il downcasting con dynamic_cast, l'unico cast con verifica a run-time: su un puntatore dà nullptr se non lecito, su un riferimento lancia bad_cast.C++ - polimorfismo →, Classi ed ereditarietà in PythonAttributi di istanza e di classe, metodi di istanza, di classe e statici; confronto e ordinamento con eq e lt; proprietà; ereditarietà, super(), override e polimorfismo; classi astratte come interfacce; eccezioni personalizzate; overloading e shadowing in Python.Classi ed ereditarietà in Python → per il confronto con Python.
Testo. Definire il concetto di ereditarietà. Descrivere approfonditamente gli strumenti disponibili in C++ per implementare tale concetto. Descrivere le differenze fra le relazioni "is a" e "has a" fra classi e illustrare tali differenze con un breve esempio di codice. Descrivere il problema dello slicing e le metodologie per evitarlo riportando un esempio di codice.
Il concetto
Ereditarietà. Meccanismo con cui una classe (derivata) acquisisce i dati e le funzioni membro di un'altra (la base) e li estende o specializza. Serve a raggruppare le caratteristiche comuni di più classi in una classe base, così le più specifiche (per esempio
Circle,Rectangle) ereditano le funzionalità comuni diShape(un insieme di punti, il disegno) e aggiungono solo ciò che le distingue. Criterio di progetto abituale: molte classi, ciascuna con poche operazioni.
Gli strumenti del C++
| strumento | funzione |
|---|---|
derivazione pubblica class Circle : public Shape |
la derivata accede ai membri public e protected della base; modella la relazione is a. Le derivazioni private e protected esistono ma si usano di rado |
protected |
membri accessibili alle classi derivate ma non all'esterno (di solito i costruttori della base) |
funzioni virtuali virtual |
permettono di ridefinire una funzione nella derivata: override; con la chiamata tramite puntatore o reference alla base si esegue la versione della classe effettiva (polimorfismo a run-time, binding dinamico, tramite la virtual table) |
override |
dichiara l'intenzione di fare override: errore di compilazione se la firma non coincide (per esempio manca const) |
funzioni virtuali pure = 0 |
la base è astratta: non si istanzia, impone l'override alle derivate |
| distruttore virtuale | obbligatorio in una base polimorfica: distrugge correttamente un oggetto derivato cancellato tramite Base* |
copia vietata nella base (= delete) |
evita lo slicing |
dynamic_cast |
downcasting con verifica a run-time (C++ - polimorfismoPolimorfismo = abilità di associare comportamenti diversi a un'unica notazione generica. In C++ ci sono due forme principali: dinamico (ereditarietà più funzioni virtuali: vincolato dall'interfaccia della classe base, binding a run-time, permette collezioni eterogenee) e statico (template: svincolato, basta che i tipi abbiano la stessa sintassi, binding a tempo di compilazione, nessun costo a run-time ma una funzione per ogni tipo); il polimorfismo statico è lo strumento della programmazione generica. Anche l'overloading di funzioni e di operatori può essere visto come polimorfismo. Nella gerarchia si naviga con l'upcasting (implicito, sempre lecito) e il downcasting con dynamic_cast, l'unico cast con verifica a run-time: su un puntatore dà nullptr se non lecito, su un riferimento lancia bad_cast.C++ - polimorfismo →) |
Regole dell'override: stesso nome, stessi tipi dei parametri e stessa constness; la funzione della base deve essere virtual. Con puntatori: un B* può puntare a un D o a un DD, e un vector<B*> gestisce una collezione di oggetti diversi: per ciascuno si chiama la funzione appropriata.
is a e has a
| relazione | significato | C++ | esempio |
|---|---|---|---|
| is a (è un) | un oggetto della derivata è un oggetto della base; non biunivoca (una forma non è per forza un cerchio) | ereditarietà pubblica | Circle è una Shape |
| has a (ha un) | un oggetto di una classe contiene un oggetto di un'altra | composizione: un dato membro | Car ha delle Wheel |
Il test: "un'automobile è un volante?" No: ha dei volanti: composizione, non eredità. Un cerchio è una forma: eredità. Sbagliare la relazione porta a gerarchie assurde (Car : public Wheel), con interfacce che non hanno senso.
#include <iostream>
#include <memory>
#include <string>
#include <vector>
using namespace std;
// ---------- ereditarieta': Circle "is a" Shape ----------
class Shape {
public:
Shape() = default;
virtual ~Shape() = default; // distruttore virtuale
Shape(const Shape&) = delete; // base di una gerarchia: copia vietata (contro lo slicing)
Shape& operator=(const Shape&) = delete;
virtual double area() const = 0; // virtuale pura: Shape e' astratta
string name() const { return name_; }
protected:
explicit Shape(string n) : name_{std::move(n)} {} // visibile alle derivate
private:
string name_ = "forma";
};
class Circle : public Shape { // ereditarieta' pubblica: Circle E' UNA Shape
public:
explicit Circle(double r) : Shape("cerchio"), r_{r} {}
double area() const override { return 3.14159 * r_ * r_; }
private:
double r_;
};
// ---------- composizione: Car "has a" Wheel ----------
class Wheel {
public:
explicit Wheel(int diametro) : diametro_{diametro} {}
int diametro() const { return diametro_; }
private:
int diametro_;
};
class Car { // Car NON e' una Wheel: ne HA quattro
public:
Car() : ruote_{Wheel(16), Wheel(16), Wheel(16), Wheel(16)} {}
int n_ruote() const { return static_cast<int>(ruote_.size()); }
int diametro_ruote() const { return ruote_[0].diametro(); }
private:
vector<Wheel> ruote_; // l'oggetto Wheel e' CONTENUTO dentro Car
};
// ---------- slicing ----------
class Base {
public:
virtual ~Base() = default;
virtual string chi() const { return "Base"; }
int a = 1;
};
class Derivata : public Base {
public:
string chi() const override { return "Derivata"; }
int b = 2; // dato membro in piu'
};
int main()
{
unique_ptr<Shape> s = make_unique<Circle>(2.0); // un puntatore alla base puo' puntare a una derivata
cout << s->name() << ": area = " << s->area() << '\n';
Car c;
cout << "Car ha " << c.n_ruote() << " ruote da " << c.diametro_ruote() << " pollici\n";
cout << "sizeof(Base) = " << sizeof(Base) << ", sizeof(Derivata) = " << sizeof(Derivata) << '\n';
Derivata d;
vector<Base> v; // vector di OGGETTI Base
v.push_back(d); // SLICING: si copia solo la parte Base
cout << "oggetto originale: " << d.chi() << ", copia nel vector<Base>: " << v[0].chi() << '\n';
vector<unique_ptr<Base>> p; // soluzione: puntatori alla base
p.push_back(make_unique<Derivata>());
cout << "vector di puntatori: " << p[0]->chi() << '\n';
const Base& r = d; // soluzione: reference (nessuna copia)
cout << "tramite reference: " << r.chi() << '\n';
return 0;
}cerchio: area = 12.5664
Car ha 4 ruote da 16 pollici
sizeof(Base) = 8, sizeof(Derivata) = 12
oggetto originale: Derivata, copia nel vector<Base>: Base
vector di puntatori: Derivata
tramite reference: Derivataunique_ptr<Shape> s = make_unique<Circle>(2.0): un puntatore alla base punta a una derivata;area()è virtuale pura, quindi la chiamata arriva aCircle::area().Shapeè astratta:Shape x;non compila.Carha unvector<Wheel>: l'oggettoWheelsta dentroCar;Carnon eredita nulla daWheel.
Lo slicing
Se Circle ha dati in più (il raggio) e si inserisce un Circle in un vector<Shape>, ogni slot può contenere solo ciò che c'è in Shape: il costruttore di copia copia solo la parte base e taglia il resto: l'oggetto perde i membri aggiunti e perde il tipo derivato, quindi anche l'override non funziona più. Nel programma sopra: sizeof(Base) = 8 e sizeof(Derivata) = 12 (in WebAssembly un puntatore è 4 byte; su un PC a 64 bit i valori sono più grandi); v.push_back(d) in un vector<Base> copia solo i primi 8 byte, e v[0].chi() stampa Base anche se d.chi() è Derivata.
Come evitarlo ("class hierarchies plus pass-by-reference and default copying do not mix"):
- disabilitare copia e assegnamento nella base (
Shape(const Shape&) = delete; Shape& operator=(const Shape&) = delete;): la riga con lo slicing diventa un errore di compilazione (push_backrichiede la copia:static assertion failed ... is_move_insertable); - usare puntatori (meglio smart pointer) alla base nei contenitori:
vector<unique_ptr<Base>>: si copia il puntatore, non l'oggetto: nell'esempiop[0]->chi()stampaDerivata; - usare reference nelle funzioni (
const Base& r = d;): nessuna copia,r.chi()stampaDerivata; - rendere la base astratta (con virtuali pure o costruttori protetti): così non si può nemmeno creare un oggetto
Baseda copiare.
Versione ripasso
- Ereditarietà = una classe derivata acquisisce dati e funzioni di una base e li estende/specializza: raggruppa le caratteristiche comuni (molte classi, poche operazioni ciascuna). Rimando: C++ - ereditarietà, funzioni virtuali e layout degli oggettiL'ereditarietà pubblica (class D : public B) esprime la relazione "is a": D eredita da B i membri public e protected e li estende; la composizione esprime invece "has a". Una classe base di una gerarchia disabilita copia e assegnamento (=delete) perché copiando una derivata in una base si ha lo slicing. Una funzione virtuale può essere ridefinita nelle derivate (override: stesso nome, tipi e constness) e la funzione chiamata si decide a run-time in base al tipo dell'oggetto, non del puntatore o del riferimento; override esplicito con la keyword override; una funzione virtuale pura (= 0) obbliga le derivate a ridefinirla e rende astratta la classe; il distruttore di una classe base polimorfa è virtuale. Il meccanismo è realizzato con una vtbl per classe (tabella di puntatori alle funzioni) e un vptr in ogni oggetto; i dati della derivata seguono quelli della base.C++ - ereditarietà, funzioni virtuali e layout degli oggetti →.
- Strumenti C++: derivazione
public(class Circle : public Shape; accesso apubliceprotecteddella base;private/protectedrare);protected;virtual+ override (stesso nome, tipi e constness; chiamata tramite puntatore/reference alla base versione della classe effettiva, via virtual table);override(errore se non coincide); virtuali pure= 0classe astratta; distruttore virtuale; copia= deletenella base;dynamic_castper il downcasting. Unvector<B*>ospita oggettiB,D,DD.
| relazione | significato | C++ | esempio |
|---|---|---|---|
| is a | la derivata è una base | ereditarietà pubblica | Circle è una Shape |
| has a | un oggetto contiene un altro | composizione (dato membro) | Car ha un vector<Wheel> |
class Circle : public Shape { public: double area() const override { return 3.14159 * r_ * r_; } /*...*/ }; // is a
class Car { vector<Wheel> ruote_; /*...*/ }; // has a- Slicing: inserire un
Derivatain unvector<Base>copia solo la parte base: si perdono i dati in più e il tipo derivato (v[0].chi()=Base, mentred.chi()=Derivata;sizeof(Base) = 8,sizeof(Derivata) = 12in wasm32). - Rimedi: (1) base con copia e assegnamento
= deleteerrore di compilazione; (2) contenitori di puntatori (vector<unique_ptr<Base>>:p[0]->chi()=Derivata); (3) reference (const Base& r = d;); (4) base astratta. - Errori tipici:
Car : public Wheel(non è una relazione is-a); dimenticarevirtual(nessun override) oconst(nessun override: usareoverride); distruttore non virtuale;vector<Shape>con oggetti derivati; derivare per riusare codice quando basta la composizione.