Salta al contenuto
Note per Studenti Esercizio - Gerarchia MyPointer, MyUniquePointer e MySharedPointer (terzo appello 2021-22, programmazione)

Esercizio - Gerarchia MyPointer, MyUniquePointer e MySharedPointer (terzo appello 2021-22, programmazione)

Esame

Prova di programmazione (60 minuti) del terzo appello 2021-22 (30 giugno 2022) del corso di Laboratorio di Programmazione, Ing. Informatica UniPD. Teoria: C++ - smart pointerGli smart pointer (classi template di ) sono oggetti che detengono un puntatore e lo deallocano automaticamente all'uscita dallo scope (RAII), eliminando memory leak e dangling pointer con un overhead minimo. unique_ptr ha un solo proprietario: non si copia (costruttore e assegnamento di copia cancellati) ma si sposta (std::move, ritorno da funzione), ha reset(), release(), get(), * e ->; due unique_ptr costruiti con lo stesso puntatore grezzo sono un errore (doppia delete) che il compilatore non vede, e si evita con make_unique. shared_ptr ha più proprietari: si copia, tiene un contatore di riferimenti (use_count) e libera l'oggetto quando l'ultimo shared_ptr viene distrutto, a un costo maggiore; weak_ptr osserva senza possedere, per rompere i cicli. auto_ptr è deprecato (rimosso in C++17).C++ - smart pointer →, 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++ - costruttori, inizializzazione, copia e spostamentouna buona interfaccia è completa e minimale, ha i costruttori giusti, supporta o proibisce la copia, usa tipi adeguati, dichiara const ciò che non modifica e libera le risorse nel distruttore; il costruttore di default (senza argomenti) esiste quando c'è un valore iniziale sensato, si ottiene con valori di default nei membri (in-class initializer) o argomenti di default, e explicit vieta la conversione implicita da un argomento; initializer_list permette la lista {a, b, c} e con le graffe ha la precedenza sul costruttore a un intero (vector{3} è un elemento, vector(3) sono tre); la copia di default è membro a membro (shallow copy) e con un puntatore a un buffer duplica il puntatore invece del contenuto (due oggetti che condividono i dati e una doppia delete), quindi servono il costruttore di copia e l'assegnamento di copia che fanno la deep copy (l'assegnamento prima alloca e copia, poi libera i vecchi dati, e deve tollerare l'auto-assegnamento); this è il puntatore all'oggetto corrente, parametro implicito delle funzioni membro; il costruttore e l'assegnamento di spostamento (T&&, rvalue reference) rubano il buffer della sorgente lasciandola vuota ma valida, il compilatore li sceglie da solo (con la copy elision a volte non servono nemmeno); i cinque (distruttore, copia, spostamento) vanno definiti insieme.C++ - costruttori, inizializzazione, copia e spostamento →; la parte di teoria dello stesso tema è in Esercizio - Smart pointer, unique_ptr e shared_ptr (primo appello 2021-22, teoria).

Testo. Progettare una gerarchia di classi formata da una classe base MyPointer e da due classi derivate, MyUniquePointer e MySharedPointer, per la gestione di puntatori a tipo int. Il comportamento delle classi derivate è analogo a quello degli smart pointer. Entrambe le classi derivate implementano: il costruttore di default; il move constructor; la funzione reset. MyUniquePointer inibisce il costruttore di copia, mentre MySharedPointer lo implementa. La funzione release (vista a lezione) non deve essere implementata. Per la gestione di MySharedPointer è necessario mantenere un registro delle locazioni di memoria puntate da tutti i puntatori creati nel sistema. Questo può essere gestito mediante una std::map in cui la chiave è la locazione di memoria puntata e il valore è il numero di puntatori che puntano a tale area. Il distruttore di MySharedPointer dovrà verificare quanti altri smart pointer puntano alla stessa area: la liberazione della memoria puntata avviene se non ne sono presenti altri; in caso contrario, il distruttore dovrà diminuire di un'unità il contatore, senza liberare la memoria. La funzione reset ha il comportamento definito dallo standard, ovvero: reset accetta un puntatore in input e fa in modo che lo smart pointer detenga tale puntatore. Se era presente un puntatore prima della chiamata di reset, la relativa memoria è gestita (cioè liberata se si tratta di MyUniquePointer o di MySharedPointer e non sono presenti altri riferimenti, oppure si riduce di una unità il contatore del numero di riferimenti se è presente più di un riferimento). Oltre a implementare la gerarchia di classi, si implementi una funzione main di test dei due tipi di smart pointer.

Progetto

Che cosa è comune alle due classi e che cosa no:

comune (nella base MyPointer) MyUniquePointer MySharedPointer
dato int* ptr_ (protetto) un registro map<int*, int> statico (condiviso da tutti gli oggetti)
accesso operator*, get(), explicit operator bool use_count()
reset(int*) dichiarata virtuale pura: la politica di liberazione è diversa libera il vecchio e detiene il nuovo rilascia il vecchio (libera solo se ultimo) e acquisisce il nuovo
copia cancellata nella base cancellata implementata: copia l'indirizzo e incrementa il contatore
spostamento trasferisce il puntatore, sorgente a nullptr idem, senza cambiare il contatore (il numero di proprietari non cambia)
distruttore virtual delete ptr_ decrementa il contatore; libera se arriva a 0

Decisioni:

Programma

mypointer.cppcpp
#include <cstdlib>
#include <iostream>
#include <map>
#include <new>
#include <utility>
using namespace std;

// ---- per il test: contiamo le allocazioni e le liberazioni di un int, per verificare che non ci siano ----
// ---- leak ne' doppie delete (le altre allocazioni, come i nodi della map, non si contano) ----
static int n_new = 0, n_delete = 0;
void* operator new(size_t s) { if (s == sizeof(int)) ++n_new; return malloc(s); }
void operator delete(void* p) noexcept { free(p); }
void operator delete(void* p, size_t s) noexcept { if (p && s == sizeof(int)) ++n_delete; free(p); }

// ======================= classe base =======================
// Contiene cio' che hanno in comune: il puntatore detenuto, l'accesso e la funzione reset.
class MyPointer {
public:
    virtual ~MyPointer() = default;                  // distruttore virtuale: si distruggono via puntatore alla base
    MyPointer(const MyPointer&) = delete;            // la base non si copia (slicing); le derivate decidono
    MyPointer& operator=(const MyPointer&) = delete;

    virtual void reset(int* p = nullptr) = 0;        // pura: ogni derivata ha la sua politica di liberazione

    int& operator*() const { return *ptr_; }
    int* get() const { return ptr_; }
    explicit operator bool() const { return ptr_ != nullptr; }

protected:
    MyPointer() : ptr_{nullptr} {}                   // protetto: la base e' astratta
    int* ptr_;
};

// ======================= possesso esclusivo =======================
class MyUniquePointer : public MyPointer {
public:
    MyUniquePointer() = default;                                  // costruttore di default: non detiene nulla
    explicit MyUniquePointer(int* p) { ptr_ = p; }                // detiene p
    MyUniquePointer(const MyUniquePointer&) = delete;             // la copia e' INIBITA
    MyUniquePointer& operator=(const MyUniquePointer&) = delete;

    MyUniquePointer(MyUniquePointer&& other) noexcept             // move constructor: trasferisce il possesso
    {
        ptr_ = other.ptr_;
        other.ptr_ = nullptr;
    }
    MyUniquePointer& operator=(MyUniquePointer&& other) noexcept
    {
        if (this != &other) { reset(other.ptr_); other.ptr_ = nullptr; }
        return *this;
    }

    void reset(int* p = nullptr) override                         // detiene p; il vecchio oggetto e' liberato
    {
        if (p == ptr_) return;                                    // reset(get()): niente da fare (e niente delete)
        delete ptr_;
        ptr_ = p;
    }
    ~MyUniquePointer() override { delete ptr_; }
};

// ======================= possesso condiviso =======================
class MySharedPointer : public MyPointer {
public:
    MySharedPointer() = default;
    explicit MySharedPointer(int* p) { acquisisci(p); }
    MySharedPointer(const MySharedPointer& other) { acquisisci(other.ptr_); }   // la copia e' IMPLEMENTATA
    MySharedPointer& operator=(const MySharedPointer& other)
    {
        if (this != &other) reset(other.ptr_);
        return *this;
    }
    MySharedPointer(MySharedPointer&& other) noexcept             // move: il contatore NON cambia
    {
        ptr_ = other.ptr_;
        other.ptr_ = nullptr;
    }
    MySharedPointer& operator=(MySharedPointer&& other) noexcept
    {
        if (this != &other) {
            rilascia();
            ptr_ = other.ptr_;
            other.ptr_ = nullptr;
        }
        return *this;
    }

    void reset(int* p = nullptr) override
    {
        if (p == ptr_) return;
        rilascia();                                               // dice addio al vecchio puntatore
        acquisisci(p);                                            // e detiene il nuovo
    }
    ~MySharedPointer() override { rilascia(); }

    int use_count() const { return ptr_ ? registro_.at(ptr_) : 0; }

private:
    void acquisisci(int* p)
    {
        ptr_ = p;
        if (ptr_) ++registro_[ptr_];                              // un puntatore in piu' su quell'area
    }
    void rilascia()
    {
        if (!ptr_) return;
        auto it = registro_.find(ptr_);
        if (--it->second == 0) {                                  // ero l'ultimo: libero la memoria
            delete ptr_;
            registro_.erase(it);
        }
        ptr_ = nullptr;
    }

    static inline map<int*, int> registro_;                       // registro: indirizzo puntato -> numero di puntatori
};

int main()
{
    cout << "== MyUniquePointer ==\n";
    {
        MyUniquePointer u(new int(5));
        cout << "*u = " << *u << '\n';
        // MyUniquePointer copia = u;                             // errore: la copia e' inibita
        MyUniquePointer v = std::move(u);                         // si puo' solo spostare
        cout << "dopo il move: u " << (u ? "detiene" : "vuoto") << ", *v = " << *v << '\n';
        v.reset(new int(7));                                      // libera il 5 e detiene il 7
        cout << "dopo reset: *v = " << *v << ", delete finora = " << n_delete << '\n';
    }
    cout << "uscito dallo scope: new = " << n_new << ", delete = " << n_delete << '\n';

    cout << "== MySharedPointer ==\n";
    int new0 = n_new, del0 = n_delete;
    {
        MySharedPointer a(new int(10));
        cout << "use_count = " << a.use_count() << '\n';
        {
            MySharedPointer b = a;                                // copia: contatore a 2
            MySharedPointer c;
            c = b;                                                // assegnamento: contatore a 3
            cout << "con b e c: use_count = " << a.use_count() << ", *c = " << *c << '\n';
        }                                                         // b e c escono: contatore torna a 1, nessuna delete
        cout << "dopo l'uscita di b e c: use_count = " << a.use_count() << ", delete = " << n_delete - del0 << '\n';

        MySharedPointer d = a;                                    // di nuovo 2
        a.reset(new int(20));                                     // a lascia il 10 (resta d) e prende il 20
        cout << "dopo a.reset(new int(20)): *a = " << *a << ", *d = " << *d
             << ", use_count(a) = " << a.use_count() << ", use_count(d) = " << d.use_count()
             << ", delete = " << n_delete - del0 << '\n';
        d.reset();                                                // d era l'ultimo del 10: lo libera
        cout << "dopo d.reset(): delete = " << n_delete - del0 << '\n';
    }
    cout << "uscito dallo scope: new = " << n_new - new0 << ", delete = " << n_delete - del0 << '\n';
    return 0;
}
== MyUniquePointer ==
*u = 5
dopo il move: u vuoto, *v = 5
dopo reset: *v = 7, delete finora = 1
uscito dallo scope: new = 2, delete = 2
== MySharedPointer ==
use_count = 1
con b e c: use_count = 3, *c = 10
dopo l'uscita di b e c: use_count = 1, delete = 0
dopo a.reset(new int(20)): *a = 20, *d = 10, use_count(a) = 1, use_count(d) = 1, delete = 0
dopo d.reset(): delete = 1
uscito dallo scope: new = 2, delete = 2

Lettura dell'uscita:

  • Unique: std::move(u) lascia u vuoto; v.reset(new int(7)) libera il 5 (delete finora = 1); alla fine dello scope v libera il 7: 2 new e 2 delete. La copia (MyUniquePointer copia = u;) non compila.
  • Shared: a →\to use_count = 1; con b = a e c = b →\to 3; usciti b e c →\to 1 e nessuna delete (delete = 0): la memoria resta a a. Poi d = a porta il contatore del 10 a 2; a.reset(new int(20)): a lascia il 10 (resta d: nessuna delete, delete = 0) e prende il 20, entrambi con use_count = 1; d.reset() è l'ultimo proprietario del 10: lo libera (delete = 1); all'uscita a libera il 20: 2 new, 2 delete.

Versione ripasso

  • Base MyPointer: int* ptr_ protetto; operator*, get(), explicit operator bool; virtual void reset(int* p = nullptr) = 0;; costruttore protetto, distruttore virtuale, copia = delete.
  • MyUniquePointer: default; explicit MyUniquePointer(int* p); copia = delete; move: ptr_ = other.ptr_; other.ptr_ = nullptr;; reset(p): if (p == ptr_) return; delete ptr_; ptr_ = p;; distruttore delete ptr_.
  • MySharedPointer: registro static inline map<int*, int> registro_ (area →\to numero di puntatori).
cpp
void acquisisci(int* p) { ptr_ = p; if (ptr_) ++registro_[ptr_]; }
void rilascia() { if (!ptr_) return; auto it = registro_.find(ptr_);
                  if (--it->second == 0) { delete ptr_; registro_.erase(it); }
                  ptr_ = nullptr; }
void reset(int* p = nullptr) override { if (p == ptr_) return; rilascia(); acquisisci(p); }
~MySharedPointer() override { rilascia(); }

Copia: acquisisci(other.ptr_) (contatore +1+1); move: trasferisce il puntatore senza toccare il registro.

  • Test con contatori su operator new/delete: Unique: *u = 5, dopo move u vuoto, reset(new int(7)) libera il 5, fine scope 2 new e 2 delete; Shared: use_count 1, con b e c 3, ritorno a 1 senza delete; d = a, a.reset(new int(20)): delete = 0 (resta d), d.reset() libera il 10, alla fine 2 new e 2 delete.
  • Classi derivate, codice essenziale:
cpp
class MyUniquePointer : public MyPointer {
public:
    MyUniquePointer() = default;
    explicit MyUniquePointer(int* p) { ptr_ = p; }
    MyUniquePointer(const MyUniquePointer&) = delete;                    // copia inibita
    MyUniquePointer(MyUniquePointer&& o) noexcept { ptr_ = o.ptr_; o.ptr_ = nullptr; }
    void reset(int* p = nullptr) override { if (p == ptr_) return; delete ptr_; ptr_ = p; }
    ~MyUniquePointer() override { delete ptr_; }
};
class MySharedPointer : public MyPointer {
public:
    MySharedPointer() = default;
    explicit MySharedPointer(int* p) { acquisisci(p); }
    MySharedPointer(const MySharedPointer& o) { acquisisci(o.ptr_); }       // copia implementata
    MySharedPointer(MySharedPointer&& o) noexcept { ptr_ = o.ptr_; o.ptr_ = nullptr; }   // il registro non cambia
    int use_count() const { return ptr_ ? registro_.at(ptr_) : 0; }
    /* reset, ~MySharedPointer, acquisisci, rilascia: vedi sopra */
};
  • Perché la base è astratta: reset virtuale pura e costruttore protetto: nessuna istanza di MyPointer; il distruttore virtuale permette di cancellare via MyPointer*.
  • Uscita del test (conteggi di new int/delete): Unique *u = 5, dopo il move u vuoto, *v = 5, dopo reset: *v = 7, delete finora = 1, fine scope new = 2, delete = 2; Shared use_count = 1, con b e c: use_count = 3, *c = 10, dopo l'uscita di b e c: use_count = 1, delete = 0, dopo a.reset(new int(20)): *a = 20, *d = 10, use_count(a) = 1, use_count(d) = 1, delete = 0, dopo d.reset(): delete = 1, fine scope new = 2, delete = 2.
  • Errori tipici: move del MySharedPointer che incrementa il contatore; reset(get()) che libera e riusa; non cancellare la chiave dal registro; base senza distruttore virtuale; copia della base non inibita (slicing); dimenticare if (ptr_) prima di ++registro_[ptr_].

Esercizi su questo argomento

Teoria collegata