Esercizio - Gerarchia MyPointer, MyUniquePointer e MySharedPointer (terzo appello 2021-22, programmazione)
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
Testo. Progettare una gerarchia di classi formata da una classe base
MyPointere da due classi derivate,MyUniquePointereMySharedPointer, per la gestione di puntatori a tipoint. Il comportamento delle classi derivate è analogo a quello degli smart pointer. Entrambe le classi derivate implementano: il costruttore di default; il move constructor; la funzionereset.MyUniquePointerinibisce il costruttore di copia, mentreMySharedPointerlo implementa. La funzionerelease(vista a lezione) non deve essere implementata. Per la gestione diMySharedPointerè necessario mantenere un registro delle locazioni di memoria puntate da tutti i puntatori creati nel sistema. Questo può essere gestito mediante unastd::mapin cui la chiave è la locazione di memoria puntata e il valore è il numero di puntatori che puntano a tale area. Il distruttore diMySharedPointerdovrà 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 funzioneresetha il comportamento definito dallo standard, ovvero:resetaccetta un puntatore in input e fa in modo che lo smart pointer detenga tale puntatore. Se era presente un puntatore prima della chiamata direset, la relativa memoria è gestita (cioè liberata se si tratta diMyUniquePointero diMySharedPointere 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 funzionemaindi 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:
- Base astratta con distruttore virtuale (
resetvirtuale pura, costruttore di default protetto): si può distruggere un'istanza via puntatore alla base e non si istanzianoMyPointernudi. La copia nella base è= delete, come in ogni gerarchia (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 →, slicing). - Registro:
static inline std::map<int*, int> registro_;condiviso da tutte le istanze. La chiave è l'indirizzo puntato, il valore quantiMySharedPointerlo detengono. acquisisci(p): impostaptr_e, se non nullo, incrementa il contatore.rilascia(): septr_non è nullo, decrementa il contatore; se arriva a zero fadeletee toglie la chiave dal registro; poiptr_ = nullptr.reset(p)èrilascia()seguito daacquisisci(p); il distruttore èrilascia(). Il controlloif (p == ptr_) return;inresetevita di liberare un'area e poi riacquisirla (reset(get())).- Move: non tocca il registro, perché il numero di puntatori che detengono l'area resta lo stesso: cambia solo chi la detiene.
- Verifica: il programma di test sostituisce
operator new/deleteper contare le allocazioni e le liberazioni diint, e controlla che non ci siano leak né doppie delete.
Programma
#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 = 2Lettura dell'uscita:
- Unique:
std::move(u)lasciauvuoto;v.reset(new int(7))libera il 5 (delete finora = 1); alla fine dello scopevlibera il 7: 2newe 2delete. La copia (MyUniquePointer copia = u;) non compila. - Shared:
ause_count = 1; conb = aec = b3; uscitibec1 e nessunadelete(delete = 0): la memoria resta aa. Poid = aporta il contatore del 10 a 2;a.reset(new int(20)):alascia il 10 (restad: nessunadelete,delete = 0) e prende il 20, entrambi conuse_count = 1;d.reset()è l'ultimo proprietario del 10: lo libera (delete = 1); all'uscitaalibera il 20: 2new, 2delete.
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;; distruttoredelete ptr_.MySharedPointer: registrostatic inline map<int*, int> registro_(area numero di puntatori).
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 ); move: trasferisce il puntatore senza toccare il registro.
- Test con contatori su
operator new/delete: Unique:*u = 5, dopomoveuvuoto,reset(new int(7))libera il 5, fine scope 2newe 2delete; Shared:use_count1, conbec3, ritorno a 1 senzadelete;d = a,a.reset(new int(20)):delete = 0(restad),d.reset()libera il 10, alla fine 2newe 2delete. - Classi derivate, codice essenziale:
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:
resetvirtuale pura e costruttore protetto: nessuna istanza diMyPointer; il distruttore virtuale permette di cancellare viaMyPointer*. - Uscita del test (conteggi di
new int/delete): Unique*u = 5, dopo il moveu vuoto, *v = 5,dopo reset: *v = 7, delete finora = 1, fine scopenew = 2, delete = 2; Shareduse_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 scopenew = 2, delete = 2. - Errori tipici: move del
MySharedPointerche 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); dimenticareif (ptr_)prima di++registro_[ptr_].