Salta al contenuto
Note per Studenti Esercizio - Polimorfismo statico e dinamico, bounded e unbounded (secondo appello 2022-23, teoria)

Esercizio - Polimorfismo statico e dinamico, bounded e unbounded (secondo appello 2022-23, teoria)

Esame
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 (o class T, equivalenti) davanti alla classe o alla funzione, e le funzioni membro fuori dalla classe si scrivono template tipo Classe::nome(...); nelle function template il compilatore deduce gli argomenti dai parametri della chiamata (myMax(i, j) invece di myMax(i, j)), ma due argomenti di tipo diverso per lo stesso T sono un errore; una specializzazione esplicita (template<> ...) fornisce il codice per un tipo particolare; i parametri interi (template<typename T, int N> struct array) danno array di dimensione fissa senza free store, più efficienti e che esprimono concetti come la tripletta RGB; poiché il compilatore deve vedere la definizione completa al punto d'uso, le definizioni si mettono nell'header (spesso in un file .hpp incluso in coda al .h).C++ - template e programmazione generica →, 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 →. La prima parte (definizione ed esempi) è già svolta nel primo appello 2021-22: Esercizio - Polimorfismo dinamico e statico (primo appello 2021-22, teoria); qui si aggiungono punti di forza e di debolezza e la distinzione bounded/unbounded.

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().

poli_dinamico.cppcpp
#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 = 6

Statico

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.

poli_statico.cppcpp
#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 = 6

Punti 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.

poli_unbounded.cppcpp
#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
beep

Con 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

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 →\to ereditarietà, dinamico. Unbounded (svincolato): nessuna base, basta la stessa sintassi →\to template, statico. Esempio: Cane e Robot non imparentati con speak(), template <typename T> void ascolta(const T& x) { cout << x.speak(); } →\to 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 virtual o il distruttore virtuale; dire che i template richiedono una base comune.
  • Uscite dei programmi: dinamico e statico stampano Circle: area = 3.14159 e Rectangle: area = 6; unbounded: bau, beep. Un'unica stampa(const Shape&) contro stampa<Circle> e stampa<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.

Esercizi su questo argomento

Teoria collegata