Esercizio - Smart pointer, unique_ptr e shared_ptr (primo appello 2021-22, teoria)
In questa pagina 3
Domanda aperta 2 del primo appello 2021-22 (25 gennaio 2022) del corso di Laboratorio di Programmazione, Ing. Informatica UniPD. Teoria: C++ - smart pointerGli smart pointer (classi template di
Testo. Descrivere le caratteristiche fondamentali degli smart pointers. Illustrare le differenze fra
unique_ptr<T>eshared_ptr<T>. Descrivere il comportamento delle seguenti funzioni della classeunique_ptr<T>:reset()erelease().
Caratteristiche degli smart pointer
Gli smart pointer ("puntatori intelligenti") sono classi template definite in <memory>. Un oggetto smart pointer detiene un puntatore e ne gestisce la vita:
- deallocano automaticamente la memoria quando escono dallo scope (il distruttore chiama
delete): niente memory leak e niente dangling pointer dovuto a unadeletedimenticata; - si usano come puntatori (
*p,p->campo), ma sono oggetti: seguono la regola RAII (C++ - eccezioni e gestione delle risorse (RAII)Un errore che non si può gestire dove avviene si segnala con throw e si gestisce con try/catch più in alto; la libreria standard ha la gerarchia std::exception (logic_error e runtime_error con le loro derivate) e le eccezioni proprie si derivano pubblicamente da una di esse; si cattura per riferimento. Le eccezioni mettono in crisi il codice che acquisisce risorse (memoria con new, file, lock, socket): se una chiamata lancia prima della delete, la risorsa si perde. La soluzione è RAII (Resource Acquisition Is Initialization): acquisire la risorsa nel costruttore di un oggetto locale e rilasciarla nel distruttore, perché all'uscita da uno scope, anche per eccezione (stack unwinding), i distruttori degli oggetti locali vengono chiamati. Garanzie di una funzione: basic (nessuna perdita, stato valido), strong (commit or rollback: se fallisce lo stato osservabile è quello di prima) e no-throw (non lancia).C++ - eccezioni e gestione delle risorse (RAII) →) e quindi liberano la risorsa anche se viene lanciata un'eccezione, senzatry/catch; - hanno un overhead presente ma molto limitato, a differenza di un garbage collector;
- esistono
unique_ptr,shared_ptr(e il deprecatoauto_ptr); differiscono nelle regole di possesso.
unique_ptr e shared_ptr
unique_ptr<T> |
shared_ptr<T> |
|
|---|---|---|
| possesso | esclusivo: un solo proprietario | condiviso: molti proprietari dello stesso oggetto |
| copia | disabilitata (costruttore e assegnamento di copia = delete) |
permessa |
| spostamento | permesso (std::move, ritorno da funzione) |
permesso |
| meccanismo | nessuno: overhead sostanzialmente nullo rispetto a un puntatore nudo | reference counting: un contatore dei shared_ptr che detengono lo stesso puntatore |
| quando libera | quando lo unique_ptr è distrutto (o reset) |
quando l'ultimo shared_ptr che lo detiene è distrutto |
| costo | trascurabile | il conteggio consuma risorse |
Limite di unique_ptr: due unique_ptr non possono puntare allo stesso oggetto. La copia è vietata dal compilatore (unique_ptr<X> q {p}; errore), ma se si inizializzano due unique_ptr con lo stesso puntatore nudo il compilatore non se ne accorge e si ha una doppia delete; il punto debole è l'esistenza del puntatore nudo, risolvibile con make_unique.
reset() e release()
| funzione | effetto |
|---|---|
reset(p) |
lo unique_ptr detiene il nuovo puntatore p; se prima ne deteneva un altro, l'oggetto puntato è distrutto (deallocato). reset() senza argomento: semplice deallocazione, lo smart pointer diventa vuoto |
release() |
rinuncia al possesso: restituisce il puntatore nudo e mette lo smart pointer a nullptr, senza distruggere l'oggetto. La responsabilità di liberarlo passa a chi lo riceve; si perde la deallocazione automatica |
#include <iostream>
#include <memory>
using namespace std;
struct Risorsa {
int id;
explicit Risorsa(int i) : id{i} { cout << " Risorsa(" << id << ") creata\n"; }
~Risorsa() { cout << " Risorsa(" << id << ") distrutta\n"; }
};
int main()
{
cout << "unique_ptr:\n";
{
unique_ptr<Risorsa> u {new Risorsa(1)};
cout << " u->id = " << u->id << ", (*u).id = " << (*u).id << '\n'; // si usa come un puntatore
// unique_ptr<Risorsa> copia = u; // errore: la copia e' disabilitata
unique_ptr<Risorsa> spostato = std::move(u); // lo spostamento si puo' fare
cout << " dopo il move: u " << (u ? "detiene" : "e' vuoto") << ", spostato " << (spostato ? "detiene" : "e' vuoto") << '\n';
spostato.reset(new Risorsa(2)); // reset(p): distrugge la Risorsa(1) e detiene la nuova
cout << " reset(new ...): ora detiene la " << spostato->id << '\n';
Risorsa* nudo = spostato.release(); // release(): restituisce il puntatore SENZA distruggere
cout << " dopo release: spostato " << (spostato ? "detiene" : "e' vuoto") << ", il puntatore nudo punta a " << nudo->id << '\n';
delete nudo; // ora la responsabilita' e' nostra
spostato.reset(); // reset() senza argomenti: semplice deallocazione (qui non c'e' nulla)
}
cout << "shared_ptr:\n";
{
shared_ptr<Risorsa> a = make_shared<Risorsa>(3);
cout << " use_count = " << a.use_count() << '\n';
{
shared_ptr<Risorsa> b = a; // la copia e' permessa: contatore dei riferimenti
cout << " dopo la copia use_count = " << a.use_count() << '\n';
} // b esce dallo scope: il contatore scende, la risorsa resta
cout << " dopo l'uscita di b use_count = " << a.use_count() << '\n';
} // a esce: contatore a 0, la risorsa e' distrutta
cout << "fine\n";
return 0;
}unique_ptr:
Risorsa(1) creata
u->id = 1, (*u).id = 1
dopo il move: u e' vuoto, spostato detiene
Risorsa(2) creata
Risorsa(1) distrutta
reset(new ...): ora detiene la 2
dopo release: spostato e' vuoto, il puntatore nudo punta a 2
Risorsa(2) distrutta
shared_ptr:
Risorsa(3) creata
use_count = 1
dopo la copia use_count = 2
dopo l'uscita di b use_count = 1
Risorsa(3) distrutta
fineLettura:
std::move(u)trasferisce il possesso:uresta vuoto,spostatodetiene laRisorsa(1). La riga con la copia (unique_ptr<Risorsa> copia = u;) non compilerebbe.spostato.reset(new Risorsa(2)): viene creata laRisorsa(2)e poi laRisorsa(1)che spostato deteneva è distrutta (compareRisorsa(1) distrutta).release()restituisce il puntatore allaRisorsa(2)e lasciaspostatovuoto; nessun distruttore è stato chiamato: ladelete nudo;esplicita è obbligatoria, altrimenti si perde la risorsa.shared_ptr:use_countva a 1, poi a 2 con la copiab, torna a 1 quandobesce dal blocco; la risorsa è distrutta solo quando escea, cioè quando il contatore arriva a 0.
Un esempio d'uso tipico (dalle slide): make_vec() che costruisce un vector<int> sull'heap può diventare unique_ptr<vector<int>> make_vec() e ritornare lo unique_ptr (spostato col move constructor): se il riempimento lancia un'eccezione, la memoria è liberata dal distruttore dello unique_ptr e non serve il try/catch con delete p; throw;.
Versione ripasso
- Smart pointer (
<memory>, classi template): detengono un puntatore e lo deallocano da soli all'uscita dallo scope (nessun leak/dangling); si usano con*e->; RAII: liberano anche con le eccezioni; overhead minimo (niente garbage collector). Tipi:unique_ptr,shared_ptr,auto_ptr(deprecato). 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 → .
unique_ptr |
shared_ptr |
|
|---|---|---|
| possesso | esclusivo | condiviso |
| copia | vietata (= delete) |
permessa |
| move | sì | sì |
| meccanismo | nessun overhead | reference counting |
| libera | alla distruzione / reset |
quando l'ultimo proprietario muore |
reset(p): lo smart pointer detienep; il vecchio oggetto puntato è distrutto; senza argomento: deallocazione.release(): restituisce il puntatore nudo e mette lo smart pointer anullptrsenza distruggere: la responsabilità passa al chiamante (delete).- Limite di
unique_ptr: dueunique_ptrdallo stesso puntatore nudo compilano ma fanno doppiadelete(rimedio:make_unique).
unique_ptr<Risorsa> u {new Risorsa(1)};
unique_ptr<Risorsa> s = std::move(u); // u vuoto; la copia non compila
s.reset(new Risorsa(2)); // distrugge la Risorsa(1)
Risorsa* nudo = s.release(); delete nudo; // s vuoto, delete a mano
shared_ptr<Risorsa> a = make_shared<Risorsa>(3); // use_count 1; con la copia b: 2; dopo b: 1- Esempio:
unique_ptr<vector<int>> make_vec()ritorna lounique_ptr(move): con un'eccezione nel riempimento nessun leak e nessuntry/catch. - Errori tipici: copiare uno
unique_ptr; usarerelease()pensando che liberi la memoria; due smart pointer dallo stesso puntatore nudo; dimenticare cheshared_ptrha un costo di conteggio.