Esercizio - Un unique_ptr minimo e make_unique (laboratorio 10)
In questa pagina 3
Testo.
- Implementare un semplice
unique_ptrche supporti solo: costruttore, distruttore,->,*,release(). Non implementare altre funzionalità (assegnamento, costruttore di copia, ecc.). - Nel modulo sugli smart pointer si è visto che una debolezza non gestibile dal compilatore è l'inizializzazione di due
unique_ptrcon lo stesso puntatore. Per risolverla si usamake_unique(), che restituisce unounique_ptrsenza rendere accessibile il puntatore; è una funzione template in cui il template specificato è il tipo puntato e gli argomenti inizializzano l'oggetto puntato. Dopo aver letto la documentazione (con attenzione all'esempio): creare conmake_unique()unounique_ptra unstd::vector<int>di 10 elementi (la dimensione è passata in costruzione); inizializzare tutti gli elementi con un ciclo; aggiungere altri 4 elementi conpush_back(); stampare tutti i valori.
Teoria: C++ - smart pointerGli smart pointer (classi template di
Esercizio 1: MiniUnique<T>
Che cosa deve fare, operazione per operazione
Uno unique_ptr è un oggetto che possiede un puntatore: ne ha la responsabilità di delete. Le cinque operazioni richieste:
Il testo dice di non implementare altro, ma una classe che possiede una risorsa e lascia le copie generate dal compilatore è sbagliata (regola dei cinque, C++ - buona interfaccia e progetti su più fileUna classe C++ ben progettata decide esplicitamente le sette operazioni essenziali (costruttore con argomenti, di default, di copia, assegnamento di copia, costruttore e assegnamento di spostamento, distruttore): se serve il distruttore perché si possiede una risorsa, servono quasi sempre anche copia e spostamento; un costruttore a un argomento definisce una conversione implicita, che si blocca con explicit; per capire quando i costruttori e il distruttore vengono chiamati si traccia una struct che stampa ogni chiamata. Un progetto su più file separa interfaccia (header .h con definizioni di classe e dichiarazioni, protetto da include guard) e implementazione (.cpp, compilati uno per uno, mai inclusi); al compilatore si passano solo i .cpp, il main sta in un file a sé; gli errori di compilazione (sintassi, tipi, dichiarazioni mancanti) vengono dal compilatore, quelli di linking (definizione mancante, due main, nessun main) dal linker.C++ - buona interfaccia e progetti su più file →): con la copia di default due MiniUnique avrebbero lo stesso puntatore e farebbero due delete. Quindi copia e assegnamento di copia sono dichiarati = delete: è una disabilitazione, non un'implementazione, ed è ciò che fa lo unique_ptr vero. Conseguenza voluta: senza costruttore di spostamento questo MiniUnique non si può nemmeno restituire da una funzione (il compilatore dice call to deleted constructor of 'MiniUnique<int>'); per farlo servirebbe l'operazione di spostamento, che il testo esclude.
#include <iostream>
#include <utility>
using namespace std;
// Esercizio 1: uno unique_ptr minimo. Solo: costruttore, distruttore, ->, *, release().
template <typename T>
class MiniUnique {
public:
explicit MiniUnique(T* p = nullptr) : ptr_{p} {} // prende possesso del puntatore
~MiniUnique() { delete ptr_; } // delete di nullptr e' lecito: non fa nulla
MiniUnique(const MiniUnique&) = delete; // due proprietari = doppia delete
MiniUnique& operator=(const MiniUnique&) = delete;
T* operator->() const { return ptr_; }
T& operator*() const { return *ptr_; }
T* release() // cede il puntatore e rinuncia al possesso
{
T* p = ptr_;
ptr_ = nullptr;
return p;
}
private:
T* ptr_;
};
struct X {
int v;
explicit X(int vv) : v{vv} { cout << " X(" << v << ")\n"; }
~X() { cout << " ~X(" << v << ")\n"; }
void hello() const { cout << " ciao da X(" << v << ")\n"; }
};
int main()
{
cout << "1) distruzione automatica\n";
{
MiniUnique<X> p{new X{1}};
p->hello(); // operator->
(*p).v += 10; // operator*
cout << " v = " << p->v << '\n';
}
cout << "2) release\n";
{
MiniUnique<X> q{new X{2}};
X* grezzo = q.release(); // q non possiede piu' nulla
cout << " q.release() restituito, grezzo->v = " << grezzo->v << '\n';
delete grezzo; // la delete passa a chi ha chiamato release
} // ~MiniUnique: ptr_ e' nullptr, nessuna delete
cout << "3) puntatore nullo\n";
{
MiniUnique<int> vuoto; // nessun oggetto: il distruttore non fa danni
cout << " creato e distrutto vuoto\n";
}
// MiniUnique<X> a{new X{3}};
// MiniUnique<X> b = a; // errore: copia cancellata
}Uscita eseguita:
1) distruzione automatica
X(1)
ciao da X(1)
v = 11
~X(11)
2) release
X(2)
q.release() restituito, grezzo->v = 2
~X(2)
3) puntatore nullo
creato e distrutto vuotoLettura della traccia:
X(1)è costruito dalnew;p->hello()usaoperator->;(*p).v += 10usaoperator*e modifica l'oggetto (da 1 a 11);~X(11)appare alla chiusura delle graffe, senza nessunadeletescritta: la fa il distruttore diMiniUnique.- Dopo
q.release()lo smart pointer è vuoto;~X(2)compare perché il programma fadelete grezzo. Alla fine del blocco~MiniUniqueeseguedelete nullptr: niente. Dimenticare ladeletedoporelease()produce un memory leak: chi usarelease()si riprende la responsabilità. - Con
nullptril distruttore non fa danni.
Esercizio 2: make_unique
make_unique<T>(args...) crea l'oggetto con new T(args...) e lo affida subito a uno unique_ptr. Il chiamante non vede mai il puntatore grezzo, quindi non può darlo a un secondo unique_ptr. Gli argomenti sono quelli del costruttore di T: make_unique<vector<int>>(10) chiama vector<int>(10), cioè un vettore di 10 elementi a zero (con le parentesi tonde del costruttore: vector<int>{10} sarebbe invece un vettore di un solo elemento di valore 10).
#include <iostream>
#include <memory>
#include <utility>
#include <vector>
using namespace std;
// Per capire make_unique: una versione minima con template variadico e perfect forwarding.
// (La libreria standard ha gia' std::make_unique: questa e' solo per vedere cosa fa.)
template <typename T, typename... Args>
unique_ptr<T> my_make_unique(Args&&... args)
{
return unique_ptr<T>(new T(std::forward<Args>(args)...));
}
int main()
{
// Esercizio 2: unique_ptr a un vector<int> di 10 elementi, senza mai vedere un puntatore grezzo
unique_ptr<vector<int>> p = make_unique<vector<int>>(10); // 10 e' l'argomento del costruttore di vector
cout << "size iniziale: " << p->size() << '\n';
for (size_t i = 0; i < p->size(); ++i) // inizializza tutti gli elementi con un ciclo
(*p)[i] = static_cast<int>(i * i);
for (int k = 0; k < 4; ++k) // aggiunge altri 4 elementi
p->push_back(100 + k);
cout << "size finale: " << p->size() << '\n';
for (int x : *p) cout << x << ' '; // stampa tutti i valori
cout << '\n';
// lo stesso con la versione scritta a mano
auto q = my_make_unique<vector<int>>(3, 7); // vector<int>(3, 7): tre elementi uguali a 7
cout << "my_make_unique: size " << q->size() << ", q[1] = " << (*q)[1] << '\n';
} // p e q liberano i vector da soliUscita eseguita:
size iniziale: 10
size finale: 14
0 1 4 9 16 25 36 49 64 81 100 101 102 103
my_make_unique: size 3, q[1] = 7Passo per passo: p->size() usa -> per arrivare al vector; (*p)[i] dereferenzia lo smart pointer e poi indicizza (p[i] non compila, perché lo unique_ptr non è un array); il ciclo scrive i quadrati i*i nelle 10 posizioni; i 4 push_back aggiungono 100..103 (il vettore può riallocare, ma lo unique_ptr punta all'oggetto vector, non al suo buffer interno, quindi il puntatore resta valido); il ciclo for (int x : *p) stampa i 14 valori.
La funzione my_make_unique del programma è solo un'illustrazione di cosa fa la libreria: un template variadico Args&&... args accetta un numero qualsiasi di argomenti e std::forward li passa al costruttore di T senza copie inutili (perfect forwarding). Con my_make_unique<vector<int>>(3, 7) si chiama vector<int>(3, 7): tre elementi uguali a 7, come mostra la riga finale. Nella pratica si usa std::make_unique (C++14).
Errori da evitare
- Dimenticare il costruttore
explicit:MiniUnique<int> p = new int;compilerebbe nascondendo l'acquisizione. - Lasciare la copia di default: doppia
deleteall'uscita. - Chiamare
release()e non fare ladelete: leak. deletesul valore diget()/release()mentre lo smart pointer possiede ancora l'oggetto: doppia deallocazione.- Costruire due smart pointer dallo stesso
new(il difetto chemake_uniqueelimina).
Versione ripasso
- Esercizio 1:
unique_ptrminimo con solo costruttore, distruttore,->,*,release(). Esercizio 2:make_unique<vector<int>>(10), ciclo di inizializzazione, 4push_back, stampa. 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 → . MiniUnique<T>: costruttoreexplicit MiniUnique(T* p = nullptr); distruttoredelete ptr_(delete nullptrnon fa nulla);operator->restituisceptr_;operator*restituisce*ptr_;release()restituisceptr_e lo azzera (ladeletepassa al chiamante). Copia e assegnamento di copia= delete(altrimenti doppiadelete); senza move non si può restituire da funzione (call to deleted constructor).
#include <iostream>
#include <utility>
using namespace std;
// Esercizio 1: uno unique_ptr minimo. Solo: costruttore, distruttore, ->, *, release().
template <typename T>
class MiniUnique {
public:
explicit MiniUnique(T* p = nullptr) : ptr_{p} {} // prende possesso del puntatore
~MiniUnique() { delete ptr_; } // delete di nullptr e' lecito: non fa nulla
MiniUnique(const MiniUnique&) = delete; // due proprietari = doppia delete
MiniUnique& operator=(const MiniUnique&) = delete;
T* operator->() const { return ptr_; }
T& operator*() const { return *ptr_; }
T* release() // cede il puntatore e rinuncia al possesso
{
T* p = ptr_;
ptr_ = nullptr;
return p;
}
private:
T* ptr_;
};
struct X {
int v;
explicit X(int vv) : v{vv} { cout << " X(" << v << ")\n"; }
~X() { cout << " ~X(" << v << ")\n"; }
void hello() const { cout << " ciao da X(" << v << ")\n"; }
};
int main()
{
cout << "1) distruzione automatica\n";
{
MiniUnique<X> p{new X{1}};
p->hello(); // operator->
(*p).v += 10; // operator*
cout << " v = " << p->v << '\n';
}
cout << "2) release\n";
{
MiniUnique<X> q{new X{2}};
X* grezzo = q.release(); // q non possiede piu' nulla
cout << " q.release() restituito, grezzo->v = " << grezzo->v << '\n';
delete grezzo; // la delete passa a chi ha chiamato release
} // ~MiniUnique: ptr_ e' nullptr, nessuna delete
cout << "3) puntatore nullo\n";
{
MiniUnique<int> vuoto; // nessun oggetto: il distruttore non fa danni
cout << " creato e distrutto vuoto\n";
}
// MiniUnique<X> a{new X{3}};
// MiniUnique<X> b = a; // errore: copia cancellata
}- Uscita:
X(1),ciao da X(1),v = 11,~X(11)alla fine dello scope; conrelease()ladeleteè del chiamante (~X(2)); connullptril distruttore non fa danni. make_unique<T>(args...):new T(args...)subito in unounique_ptr, senza puntatore grezzo accessibile;make_unique<vector<int>>(10)= 10 elementi; si accede con(*p)[i]ep->push_back(...). Uscita:size iniziale: 10,size finale: 14, valori0 1 4 9 16 25 36 49 64 81 100 101 102 103.- Variadico:
template <typename T, typename... Args> unique_ptr<T> my_make_unique(Args&&... args)constd::forward;my_make_unique<vector<int>>(3, 7)= tre elementi uguali a 7. - Errori tipici: costruttore non
explicit, copia di default,release()senzadelete, due smart pointer dallo stessonew,p[i]invece di(*p)[i].