Esercizio - Polimorfismo statico e dinamico, bounded e unbounded (secondo appello 2022-23, teoria)
In questa pagina 5
Domanda aperta 1 del secondo appello 2022-23 del corso di Laboratorio di Programmazione, Ing. Informatica UniPD. Teoria: 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 →, C++ - template e programmazione genericaun template permette di usare un tipo (o un intero noto in compilazione) come parametro di una classe o di una funzione; il compilatore genera il codice per ogni tipo per cui il template è specializzato (template instantiation, a tempo di compilazione): il codice è efficiente ma gli errori sono lunghi e oscuri, e il compilatore controlla sia il template sia il suo argomento; la notazione è template
Testo. Illustrare il concetto di polimorfismo e le due tipologie viste a lezione (polimorfismo statico e dinamico), aggiungendo un esempio in codice C++ per ognuna. Evidenziare i punti di forza e debolezza delle due tipologie. Spiegare cosa si intende per polimorfismo bounded e unbounded.
Il concetto
Polimorfismo. Capacità di associare comportamenti diversi alla stessa notazione generica (Vandevoorde e Josuttis, "C++ Templates"; i termini di Stroustrup sono più controintuitivi, quindi il corso usa questi). Una stessa chiamata (
forma.area()) produce risultati diversi secondo l'oggetto.
Le due tipologie:
- Polimorfismo dinamico: ereditarietà e funzioni virtuali; la funzione da eseguire si decide a tempo di esecuzione (run-time).
- Polimorfismo statico: template; la funzione da eseguire si decide a tempo di compilazione.
Dinamico
Si identifica un insieme di funzionalità comuni a classi collegate e se ne fa una classe base con funzioni virtuali; ogni derivata le specializza con l'override. Un'unica funzione (stampa(const Shape&)) lavora con tutte le forme, e la differenziazione è delegata alle varie area().
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
// Polimorfismo DINAMICO: ereditarieta' + funzioni virtuali, binding a run-time.
class Shape {
public:
virtual double area() const = 0; // virtuale pura: interfaccia comune, classe astratta
virtual const char* name() const = 0;
virtual ~Shape() = default; // distruttore virtuale: si cancella via puntatore alla base
Shape() = default;
Shape(const Shape&) = delete; // niente copia in una gerarchia (slicing)
Shape& operator=(const Shape&) = delete;
};
class Circle : public Shape {
public:
explicit Circle(double r) : r_{r} {}
double area() const override { return 3.14159 * r_ * r_; }
const char* name() const override { return "Circle"; }
private:
double r_;
};
class Rectangle : public Shape {
public:
Rectangle(double w, double h) : w_{w}, h_{h} {}
double area() const override { return w_ * h_; }
const char* name() const override { return "Rectangle"; }
private:
double w_, h_;
};
// UNA sola funzione serve per tutte le forme: la chiamata area() e' risolta a run-time
void stampa(const Shape& s)
{
cout << s.name() << ": area = " << s.area() << '\n';
}
int main()
{
vector<unique_ptr<Shape>> forme; // collezione eterogenea tramite puntatori alla base
forme.push_back(make_unique<Circle>(1.0));
forme.push_back(make_unique<Rectangle>(2.0, 3.0));
for (const auto& f : forme) stampa(*f); // la scelta di Circle::area o Rectangle::area avviene qui
// Shape s; // errore: Shape e' astratta
return 0;
}Circle: area = 3.14159
Rectangle: area = 6Statico
Le classi sono definite indipendentemente; devono però poter gestire la stessa sintassi (qui name() e area()). Il polimorfismo si realizza specificando una classe o funzione template con tipi diversi: stampa<Circle>, stampa<Rectangle> sono due funzioni distinte.
#include <iostream>
using namespace std;
// Polimorfismo STATICO: template, binding a compile-time. Nessuna classe base comune.
class Circle {
public:
explicit Circle(double r) : r_{r} {}
double area() const { return 3.14159 * r_ * r_; }
const char* name() const { return "Circle"; }
private:
double r_;
};
class Rectangle {
public:
Rectangle(double w, double h) : w_{w}, h_{h} {}
double area() const { return w_ * h_; }
const char* name() const { return "Rectangle"; }
private:
double w_, h_;
};
// Esistono tante stampa<T> quante sono le forme usate: stampa<Circle>, stampa<Rectangle>, ...
template <typename Shape>
void stampa(const Shape& s)
{
cout << s.name() << ": area = " << s.area() << '\n'; // basta che Shape abbia name() e area()
}
int main()
{
Circle c {1.0};
Rectangle r {2.0, 3.0};
stampa(c); // istanzia stampa<Circle>
stampa(r); // istanzia stampa<Rectangle>
// stampa(42); // errore di compilazione: int non ha area()
return 0;
}Circle: area = 3.14159
Rectangle: area = 6Punti di forza e di debolezza
| dinamico | statico | |
|---|---|---|
| forza | scelta a run-time: si possono avere collezioni eterogenee (vector<unique_ptr<Shape>>), tipi noti solo durante l'esecuzione; l'interfaccia è esplicita (la classe base, e override la fa controllare); codice compilato una sola volta |
nessun costo a run-time: chiamate dirette, inlinabili, ottimizzabili; funziona anche con tipi built-in e senza gerarchia (int, string); controlli di tipo a tempo di compilazione; base della programmazione generica e della STL |
| debolezza | costo: indirezione tramite vptr e virtual table, un puntatore in più per oggetto, nessun inlining; richiede puntatori o reference alla base (e il rischio di slicing); tutte le classi devono derivare dalla base (invasivo, impossibile per int o per classi di terzi); la scelta tardiva sposta gli errori a run-time |
codice generato in più (una versione per tipo), compilazioni lunghe; errori poco leggibili; il tipo deve essere noto in compilazione: niente collezioni eterogenee; definizioni negli header; requisiti impliciti sul tipo (nessuna interfaccia dichiarata) |
Bounded e unbounded
| polimorfismo bounded (vincolato) | polimorfismo unbounded (svincolato) | |
|---|---|---|
| strumento | ereditarietà (dinamico) | template (statico) |
| interfaccia | vincolata dalla classe base: le derivate devono rispettare l'interfaccia (le funzioni virtuali) della base | non predeterminata: non esiste un corrispondente della classe base; basta che i tipi forniscano la sintassi richiesta |
| relazione fra i tipi | derivano da una base comune | tipi senza alcuna relazione |
| binding | dinamico (run-time) | statico (compile-time) |
Il programma seguente mostra il polimorfismo unbounded: Cane e Robot non hanno alcuna parentela e Robot ha perfino un dato in più; basta che entrambi abbiano speak() perché il template ascolta funzioni con tutti e due.
#include <iostream>
#include <string>
using namespace std;
// Due classi SENZA alcuna parentela: non c'e' una classe base, ma hanno la stessa sintassi (speak()).
struct Cane { string speak() const { return "bau"; } };
struct Robot { string speak() const { return "beep"; } int batteria = 90; }; // ha anche altro
// Polimorfismo statico UNBOUNDED: non esiste un'interfaccia dichiarata, basta che T abbia speak()
template <typename T>
void ascolta(const T& x)
{
cout << x.speak() << '\n';
}
int main()
{
ascolta(Cane{}); // istanza ascolta<Cane>
ascolta(Robot{}); // istanza ascolta<Robot>
return 0;
}bau
beepCon il dinamico (bounded) servirebbe una base comune (class Parlante { virtual string speak() const = 0; }) e Cane e Robot dovrebbero derivarne. Altre forme che si possono vedere come polimorfismo: l'overloading di funzioni e di operatori.
Versione ripasso
- Polimorfismo = comportamenti diversi associati alla stessa notazione generica (Vandevoorde/Josuttis). Dinamico: ereditarietà + funzioni virtuali, run-time. Statico: template, compile-time. Rimando: 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 →.
- Esempio dinamico:
class Shape { public: virtual double area() const = 0; virtual ~Shape() = default; };conCircle/Rectangleche fanno override;void stampa(const Shape& s) { cout << s.area(); }(una funzione sola);vector<unique_ptr<Shape>>. - Esempio statico:
template <typename Shape> void stampa(const Shape& s) { cout << s.area(); }con classi senza base comune;stampa<Circle>,stampa<Rectangle>.
| dinamico | statico | |
|---|---|---|
| forza | collezioni eterogenee, tipo noto a run-time, interfaccia esplicita | nessun costo a run-time, inlining, anche tipi built-in, controlli in compilazione |
| debolezza | indirezione (vptr), nessun inlining, puntatori/reference (slicing), tutte le classi derivano dalla base |
codice generato in più, errori oscuri, tipo noto in compilazione (no collezioni eterogenee), requisiti impliciti |
- Bounded (vincolato): interfaccia delle derivate vincolata dalla classe base ereditarietà, dinamico. Unbounded (svincolato): nessuna base, basta la stessa sintassi template, statico. Esempio:
CaneeRobotnon imparentati conspeak(),template <typename T> void ascolta(const T& x) { cout << x.speak(); }bau,beep. - Altre forme: overloading di funzioni e operatori.
- Errori tipici: dire che il polimorfismo statico è "più lento"; confondere bounded/unbounded con dinamico/statico (sono le due facce: vincolo di interfaccia contro momento del binding); dimenticare
virtualo il distruttore virtuale; dire che i template richiedono una base comune. - Uscite dei programmi: dinamico e statico stampano
Circle: area = 3.14159eRectangle: area = 6; unbounded:bau,beep. Un'unicastampa(const Shape&)controstampa<Circle>estampa<Rectangle>(due funzioni generate). - Altre forme di polimorfismo: overloading di funzioni e di operatori; programmazione generica = codice per tipi diversi forniti dopo, purché soddisfino requisiti sintattici e semantici.