C++ - eccezioni e gestione delle risorse (RAII)
In questa pagina 4
Le eccezioni servono per segnalare un errore da un punto in cui non si può risolvere a uno in cui si può. Il problema di fondo è che interrompono il flusso normale: il codice che segue la throw non viene eseguito, e se quel codice doveva restituire una risorsa (memoria, un file, un lock) la risorsa resta occupata. Questa nota tratta le due cose insieme: come si lanciano e catturano eccezioni, e come si scrive codice che resta corretto quando ne arrivano. Per le eccezioni in un linguaggio di alto livello si confronti Eccezioni in PythonEccezioni e traceback, eccezioni predefinite più comuni, try/except/else/finally, raise per segnalare errori, propagazione lungo le chiamate.Eccezioni in Python →; per l'allocazione dinamica e le sue perdite C++ - allocazione dinamica della memoria con new e deletela memoria dinamica si prende dal free store (heap) con new, che restituisce un puntatore all'oggetto o al primo elemento dell'array (new T, new T[n], con inizializzatori {}), e si restituisce con delete (per new) e delete[] (per new[]); il puntatore non sa quanti elementi ci sono, la memoria non ha nome e non ha controllo dei limiti; in C++ non c'è garbage collector (scelta per efficienza e controllo), quindi un'area che perde ogni riferimento diventa garbage e produce un memory leak, e gli errori tipici sono la doppia cancellazione e il dangling pointer (si mette il puntatore a nullptr dopo delete); il distruttore ~T() è chiamato dal compilatore quando l'oggetto esce dallo scope o con delete e libera le risorse, per cui una classe come vector acquisisce la memoria nel costruttore e la rilascia nel distruttore; new alloca e chiama il costruttore, delete chiama il distruttore e libera; operator[] con due versioni (T& e const) dà accesso in lettura e scrittura al buffer; si alloca dinamicamente solo se serve davvero (memoria grande, dimensione nota a run-time, durata controllata).C++ - allocazione dinamica della memoria con new e delete →.
Lanciare e catturare
Una funzione che non riesce a fare il suo lavoro lancia (throw) un oggetto; il controllo risale lungo lo stack delle chiamate fino al primo blocco try il cui catch accetta quel tipo.
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
using namespace std;
class Date {
public:
class Invalid {}; // tipo dedicato: non standard
Date(int yy, int mm, int dd) : y{yy}, m{mm}, d{dd} { if (!is_valid()) throw Invalid{}; }
private:
bool is_valid() const { return y > 0 && m >= 1 && m <= 12 && d >= 1 && d <= 31; }
int y, m, d;
};
int dividi(int a, int b)
{
if (b == 0) throw invalid_argument("divisione per zero"); // eccezione standard
return a / b;
}
int main()
{
try {
Date ok{2026, 10, 14};
Date no{2026, 13, 1}; // lancia Date::Invalid
cout << "non arrivo qui\n";
} catch (Date::Invalid) {
cout << "data non valida\n";
}
try {
cout << dividi(10, 2) << '\n';
cout << dividi(1, 0) << '\n'; // lancia invalid_argument
} catch (const exception& e) { // cattura TUTTE le standard, per riferimento
cout << "catch: " << e.what() << '\n';
}
vector<int> v(3);
try {
// v[10] = 1; // operator[]: nessun controllo, comportamento indefinito
v.at(10) = 1; // at(): controlla e lancia out_of_range
} catch (const out_of_range&) {
cout << "at(10): fuori dai limiti\n";
}
}Il programma compila per Linux senza avvisi; non è stato eseguito (le eccezioni non funzionano in WebAssembly, che è l'ambiente di prova), quindi l'uscita è dedotta a mano:
data non valida
5
catch: divisione per zero
at(10): fuori dai limitiSi noti che nella seconda prova dividi(1, 0) lancia prima che cout stampi qualcosa, quindi la riga del risultato non compare. Regole:
throw expr;copia l'oggetto in una zona speciale e abbandona la funzione corrente; vengono distrutti gli oggetti locali di ogni scope lasciato (stack unwinding), finché uncatchcompatibile cattura l'eccezione. Se nessuno la cattura, il programma termina (std::terminate).- I
catchsi provano in ordine: dal più specifico al più generale.catch (...)cattura tutto. - Dentro un
catch,throw;(senza argomento) rilancia la stessa eccezione. - Si cattura per riferimento (
const exception&); catturare per valore (catch (exception e)) causerebbe lo slicing visto in 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 → e perderebbe il messaggio della derivata. vector::at()controlla i limiti e lanciaout_of_range;operator[]non controlla (accesso fuori limiti = comportamento indefinito). Il controllo "su richiesta" è una scelta di efficienza della libreria.- Un distruttore non deve lanciare: durante lo stack unwinding un'altra eccezione farebbe terminare il programma.
Eccezioni dedicate ed eccezioni standard
Una classe di eccezione dedicata come Date::Invalid è comoda ma non standard: ogni catch deve conoscerne l'esistenza. Per standardizzare la libreria standard fornisce std::exception (header <exception>, i derivati in <stdexcept>), da cui derivano tutte le altre. La classe ha costruttore e copia, il distruttore virtuale e la funzione virtuale what(), che restituisce una stringa descrittiva.
exception
├── logic_error (violazione di precondizioni o invarianti: errore di logica del programmatore)
│ ├── invalid_argument
│ ├── domain_error
│ ├── length_error
│ └── out_of_range
├── runtime_error (eventi esterni al programma, difficili da prevedere)
│ ├── range_error
│ ├── overflow_error
│ ├── underflow_error
│ └── ios_base::failure (errori di I/O)
├── bad_alloc (new fallito)
└── bad_cast (dynamic_cast su riferimento fallito)Il tipico utilizzo è try { ... } catch (std::exception& e) { std::cerr << e.what() << '\n'; }: l'eccezione catturata è ora standard, e ogni derivata finisce nello stesso catch.
È possibile derivare una propria eccezione per aggiungere dati e personalizzare what():
#include <iostream>
#include <stdexcept>
using namespace std;
class My_error : public runtime_error { // public: altrimenti catch(exception&) non lo vede
public:
explicit My_error(int x) : runtime_error{"My_error"}, error_value{x} {}
int error_value;
};
int main()
{
try {
throw My_error{42};
} catch (const My_error& e) { // dal piu' specifico al piu' generale
cout << "My_error, valore " << e.error_value << ", what() = " << e.what() << '\n';
} catch (const exception& e) {
cout << "exception: " << e.what() << '\n';
}
try {
throw My_error{7};
} catch (const exception& e) { // anche una base basta: catch per riferimento
cout << "catturata come exception: " << e.what() << '\n';
}
}Uscita attesa (dedotta, il programma compila senza avvisi per Linux): My_error, valore 42, what() = My_error e poi catturata come exception: My_error.
Attenzione, errore nelle slide. Le slide scrivono
class My_error : runtime_error { ... }. In unaclassl'ereditarietà senza specificatore è privata:My_errornon è convertibile inexceptionfuori dalla classe, quindicatch (exception&)non la cattura e l'eccezione, se nessun altro handler la prende, termina il programma. Va scrittoclass My_error : public runtime_error, come sopra.
Le risorse e le eccezioni
Una risorsa è qualcosa che si acquisisce, si usa e si rilascia: memoria nel free store, file aperti, lock, thread, socket, finestre. Il ciclo acquisizione-uso-rilascio è già un problema senza eccezioni; con le eccezioni diventa subdolo. Il caso tipico:
void suspicious(int s, int x)
{
int* p = new int[s];
// ...
delete[] p;
}La memoria è rilasciata? Dipende da cosa c'è nei ...:
nei ... |
la delete[] viene eseguita? |
motivo |
|---|---|---|
if (x) p = q; |
rilascia la memoria sbagliata | il valore di p è cambiato: il blocco originale è perso (memory leak) |
if (x) return; |
no | il flusso di esecuzione non arriva alla delete[] |
if (x) p[x] = v.at(x); con v vuoto |
no | at() lancia out_of_range: la funzione viene abbandonata |
Il terzo caso è l'effetto delle eccezioni: non è un problema solo di eccezioni, ma di gestione delle risorse, che la throw rende visibile anche nel codice apparentemente lineare.
Soluzione 1: try/catch che rilascia e rilancia
void suspicious(int s, int x)
{
int* p = new int[s];
vector<int> v;
try {
if (x) p[x] = v.at(x);
} catch (...) {
delete[] p;
throw; // rilancia l'eccezione
}
delete[] p;
}Funziona, ma il codice si duplica (due delete[]) e se si estende l'approccio bisogna proteggere ogni accesso al vettore e ogni allocazione: è difficile da leggere e da mantenere.
Soluzione 2: RAII
Usando solo std::vector al posto dell'allocazione dinamica il problema sparisce:
void f(vector<int>& v, int s)
{
vector<int> p(s);
vector<int> q(s);
// ...
}Perché è risolto? Perché la memoria è acquisita in fase di costruzione e liberata in fase di distruzione: costruttore e distruttore di vector risolvono il problema in modo semplice. Quando il flusso di esecuzione lascia uno scope, per qualsiasi motivo (fine normale, return, eccezione), vengono invocati i distruttori di tutti gli oggetti locali e dei loro sotto-oggetti.
Definizione (RAII, Resource Acquisition Is Initialization). Tecnica per cui una risorsa si acquisisce nel costruttore e si rilascia nel distruttore di un oggetto: l'oggetto esiste solo se è stato possibile acquisire la risorsa. Vale per la memoria, ma anche per socket, buffer di I/O (compresi i file), lock.
Per vedere il meccanismo, una classe Guard che stampa acquisizione e rilascio:
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
struct Guard { // una "risorsa" che stampa acquisizione e rilascio
string nome;
explicit Guard(string n) : nome{n} { cout << "acquisisco " << nome << '\n'; }
~Guard() { cout << "rilascio " << nome << '\n'; }
Guard(const Guard&) = delete;
Guard& operator=(const Guard&) = delete;
};
void f(bool guasto)
{
Guard a{"A"};
Guard b{"B"};
if (guasto) throw runtime_error("guasto");
cout << "fine di f\n";
}
int main()
{
try {
f(false);
f(true);
cout << "non arrivo qui\n";
} catch (const exception& e) {
cout << "catch: " << e.what() << '\n';
}
}Uscita (compila per Linux; l'ordine dei rilasci è stato verificato in WebAssembly con una versione che esce con return invece di throw, perché lo stack unwinding rilascia nello stesso ordine; la riga catch: è dedotta):
acquisisco A
acquisisco B
fine di f
rilascio B
rilascio A
acquisisco A
acquisisco B
rilascio B
rilascio A
catch: guastoNella seconda chiamata la throw abbandona f: prima di entrare nel catch il C++ distrugge b e poi a (ordine inverso alla costruzione), quindi le due righe rilascio compaiono prima di catch:. Nessuna delete, nessun try in f.
Quando l'oggetto deve "uscire" dallo scope
RAII si basa sull'uscita dallo scope. Ma a volte l'oggetto deve sopravvivere: tipicamente una funzione che costruisce un oggetto grande e lo ritorna con un puntatore. (Allocare dinamicamente un vector è di solito una cattiva pratica; qui è solo un esempio in cui il riempimento può lanciare.)
vector<int>* make_vec()
{
vector<int>* p = new vector<int>;
// ... riempimento: può lanciare!
return p; // il chiamante deve fare delete
}Se il riempimento lancia, return p non viene eseguito e il vector è perso. Il comportamento desiderato: se non ci sono eccezioni make_vec restituisce il puntatore, se ci sono non deve causare perdite. Con il try/catch della soluzione 1 si ottiene (make_vec1 qui sotto); con uno smart pointer si collega uscita dallo scope e deallocazione senza try (make_vec2), vedi C++ - smart pointerGli smart pointer (classi template di
#include <iostream>
#include <memory>
#include <stdexcept>
#include <vector>
using namespace std;
void riempi(vector<int>& v, bool guasto)
{
for (int i = 0; i < 3; ++i) v.push_back(i);
if (guasto) throw runtime_error("riempimento fallito");
}
// versione 1 (basic guarantee): try/catch che libera e rilancia; il chiamante deve fare delete
vector<int>* make_vec1(bool guasto)
{
vector<int>* p = new vector<int>;
try {
riempi(*p, guasto);
} catch (...) {
delete p;
throw;
}
return p;
}
// versione 2: unique_ptr, nessun try/catch e nessuna delete nel chiamante
unique_ptr<vector<int>> make_vec2(bool guasto)
{
unique_ptr<vector<int>> p{new vector<int>};
riempi(*p, guasto); // se lancia, ~unique_ptr elimina il vector
return p; // move: la proprieta' passa al chiamante
}
int main()
{
vector<int>* a = make_vec1(false);
cout << "make_vec1: " << a->size() << " elementi\n";
delete a; // responsabilita' del chiamante
auto b = make_vec2(false); // niente delete: ci pensa unique_ptr
cout << "make_vec2: " << b->size() << " elementi\n";
try { make_vec2(true); } catch (const runtime_error& e) { cout << "errore: " << e.what() << '\n'; }
}Uscita attesa (dedotta; compila per Linux senza avvisi): make_vec1: 3 elementi, make_vec2: 3 elementi, errore: riempimento fallito.
Per misurare le perdite si può contare gli oggetti vivi. Il programma seguente confronta i tre approcci (new nudo, try/catch, unique_ptr) quando la funzione lancia:
#include <iostream>
#include <memory>
#include <stdexcept>
#include <vector>
using namespace std;
struct Res { // conta quanti oggetti sono vivi
static int vivi;
Res() { ++vivi; }
~Res() { --vivi; }
};
int Res::vivi = 0;
void guasto() { throw runtime_error("guasto"); }
// 1) puntatore grezzo: se guasto() lancia, la delete non viene mai eseguita
void con_new(bool x)
{
Res* p = new Res;
if (x) guasto();
delete p;
}
// 2) soluzione 1: try/catch che rilascia e rilancia (codice duplicato)
void con_catch(bool x)
{
Res* p = new Res;
try {
if (x) guasto();
} catch (...) {
delete p;
throw; // rilancia la stessa eccezione
}
delete p;
}
// 3) soluzione 2 (RAII): la risorsa e' posseduta da un oggetto locale
void con_unique(bool x)
{
unique_ptr<Res> p{new Res};
if (x) guasto();
} // ~unique_ptr elimina Res in ogni caso
void prova(const char* nome, void (*f)(bool))
{
int prima = Res::vivi;
try { f(true); } catch (const exception&) {}
cout << nome << ": oggetti persi = " << Res::vivi - prima << '\n';
}
int main()
{
prova("con_new ", con_new); // perdita: 1
prova("con_catch ", con_catch); // nessuna perdita: 0
prova("con_unique", con_unique); // nessuna perdita: 0
}Uscita attesa: con_new : oggetti persi = 1, con_catch : oggetti persi = 0, con_unique: oggetti persi = 0. (Il caso della perdita con uscita anticipata, con un return al posto della throw, è stato eseguito in WebAssembly: con_new lascia 1 oggetto vivo, con_unique nessuno in più. Il resto è dedotto.)
Le garanzie
Le funzioni che possono ricevere eccezioni da ciò che chiamano si classificano per ciò che promettono:
| garanzia | promessa |
|---|---|
| basic (di base) | se lancia, non ci sono perdite di risorse e gli oggetti restano in uno stato valido (ma non necessariamente quello di prima) |
| strong (forte) | in più, se fallisce tutti i valori osservabili (non locali alla funzione) sono come prima della chiamata: commit or rollback |
| no-throw | la funzione non lancia mai eccezioni (si dichiara noexcept) |
La tecnica del try/catch con rilancio è la basic guarantee; la libreria standard la fornisce per i suoi contenitori. make_vec1 offre sicuramente la basic, e anche la strong se il riempimento non ha altri effetti collaterali. Il modo standard di ottenere la strong è fare prima tutto ciò che può fallire su una copia di lavoro e poi scambiare con operazioni noexcept (idioma copy and swap):
#include <iostream>
#include <new>
#include <utility>
using namespace std;
class Buffer { // array dinamico di int
public:
static bool guasto; // simula il fallimento dell'allocazione
explicit Buffer(int n, int v = 0) : n_{n}, d_{alloc(n)} { for (int i = 0; i < n_; ++i) d_[i] = v; }
Buffer(const Buffer& b) : n_{b.n_}, d_{alloc(b.n_)} { for (int i = 0; i < n_; ++i) d_[i] = b.d_[i]; }
~Buffer() { delete[] d_; }
// Assegnamento STRONG: prima si fa tutto cio' che puo' fallire su una copia di lavoro,
// poi si scambiano i puntatori con operazioni che non lanciano (commit).
Buffer& operator=(const Buffer& b)
{
Buffer copia{b}; // puo' lanciare: *this e' ancora intatto
swap(copia); // no-throw: commit
return *this; // ~Buffer di 'copia' libera i vecchi dati
}
void swap(Buffer& altro) noexcept { std::swap(n_, altro.n_); std::swap(d_, altro.d_); }
int size() const { return n_; }
int operator[](int i) const { return d_[i]; }
private:
static int* alloc(int n) { if (guasto) throw bad_alloc{}; return new int[n]; }
int n_;
int* d_;
};
bool Buffer::guasto = false;
int main()
{
Buffer a(3, 1), b(5, 2);
Buffer::guasto = true; // da qui ogni new fallisce
try {
a = b; // fallisce nella copia di lavoro
cout << "non arrivo qui\n";
} catch (const bad_alloc&) {
cout << "assegnamento fallito\n";
}
Buffer::guasto = false;
cout << "a: size=" << a.size() << " a[0]=" << a[0] << '\n'; // a e' esattamente com'era
a = b; // ora riesce
cout << "a: size=" << a.size() << " a[0]=" << a[0] << '\n';
}Uscita attesa: assegnamento fallito, a: size=3 a[0]=1, a: size=5 a[0]=2. La logica di swap (senza il caso di fallimento) è stata eseguita in WebAssembly: dopo a = b si ha size=5 e a[0]=2, e anche l'auto-assegnamento a = a funziona. Il disastro che si evita: un assegnamento "ingenuo" che fa delete[] d_; prima di d_ = new int[...], se la new lancia, lascia d_ che punta a memoria già liberata e n_ incoerente; alla distruzione si libera due volte. Non rispetta nemmeno la basic guarantee.
swap è la funzione noexcept: scambiare due interi e due puntatori non può fallire, ed è questo che rende possibile il commit.
Domande d'esame
1. Illustrare un caso in cui chiamare una funzione che può lanciare un'eccezione crea problemi alla memoria allocata dinamicamente. Cosa sono strong, basic e no-throw guarantee e come risolvono il problema?
Traccia. int* p = new int[s]; ... v.at(x) ...; delete[] p;: se at() lancia, la delete[] non viene eseguita e la memoria è persa. Basic: try { ... } catch (...) { delete[] p; throw; }, nessuna perdita e stato valido. Strong: in più, in caso di fallimento lo stato osservabile è quello di prima (commit or rollback: lavoro su una copia, poi scambio). No-throw: la funzione non lancia (noexcept), permette di costruire il commit. Soluzione migliore: RAII, per esempio vector o unique_ptr.
2. Cos'è RAII? Perché vector<int> p(s); è più sicuro di int* p = new int[s];?
Traccia. Risorsa acquisita nel costruttore e rilasciata nel distruttore; l'uscita dallo scope (anche per eccezione) chiama i distruttori degli oggetti locali. vector libera da solo la memoria, il puntatore grezzo richiede una delete[] che l'uscita anticipata o l'eccezione saltano.
3. Gerarchia std::exception: che differenza c'è tra logic_error e runtime_error? Come si definisce un'eccezione propria? Perché si cattura per riferimento?
Traccia. logic_error: precondizioni e invarianti violati (bug); runtime_error: eventi esterni difficili da prevedere. Si deriva pubblicamente e si può ridefinire what(). Per riferimento per evitare lo slicing e mantenere il tipo dinamico; catch (const exception&) prende tutte le derivate.
Versione ripasso
Eccezioni:
throw obj;abbandona la funzione, stack unwinding (distruttori degli oggetti locali), primocatchcompatibile; nessuno →std::terminate.catchprovati in ordine, dal più specifico;catch (...)prende tutto;throw;rilancia. Si cattura per riferimento (const exception&): per valore c'è lo slicing. Un distruttore non deve lanciare. Confronto: Eccezioni in PythonEccezioni e traceback, eccezioni predefinite più comuni, try/except/else/finally, raise per segnalare errori, propagazione lungo le chiamate.Eccezioni in Python →.std::exception:what()virtuale. Derivanologic_error(precondizioni e invarianti:invalid_argument,domain_error,length_error,out_of_range) eruntime_error(eventi esterni:range_error,overflow_error,underflow_error,ios_base::failure); poibad_alloc,bad_cast.vector::at()lanciaout_of_range,operator[]non controlla.Eccezione propria:
class My_error : public runtime_error(le slide scrivono: runtime_error= ereditarietà privata, checatch (exception&)non vede).Risorse: memoria, file, lock, thread, socket.
new...delete[]è saltata dareturnanticipato, dap = q, da unathrowin mezzo.Soluzione 1:
try { ... } catch (...) { delete[] p; throw; }: funziona ma duplica codice e non scala.RAII: acquisire nel costruttore, rilasciare nel distruttore;
vector<int> p(s);risolve il problema; all'uscita dallo scope si distruggono gli oggetti in ordine inverso. Per oggetti che devono uscire dallo scope: smart pointer (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_ptrrestituito per move.Garanzie: basic = nessuna perdita, stato valido; strong = commit or rollback, stato osservabile come prima (idioma: lavorare su una copia e poi
swapnoexcept); no-throw = non lancia. STL: almeno basic.Assegnamento: prima si alloca la copia, poi si libera il vecchio: mai
delete[]prima dellanew.Errori tipici: catch per valore, ereditarietà privata dall'eccezione standard,
deletesaltata dall'eccezione, distruttore che lancia,operator[]al posto diat()aspettandosi il controllo.Codice (raii/raii.cpp):
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
struct Guard { // una "risorsa" che stampa acquisizione e rilascio
string nome;
explicit Guard(string n) : nome{n} { cout << "acquisisco " << nome << '\n'; }
~Guard() { cout << "rilascio " << nome << '\n'; }
Guard(const Guard&) = delete;
Guard& operator=(const Guard&) = delete;
};
void f(bool guasto)
{
Guard a{"A"};
Guard b{"B"};
if (guasto) throw runtime_error("guasto");
cout << "fine di f\n";
}
int main()
{
try {
f(false);
f(true);
cout << "non arrivo qui\n";
} catch (const exception& e) {
cout << "catch: " << e.what() << '\n';
}
}- Codice (make_vec/make_vec.cpp):
#include <iostream>
#include <memory>
#include <stdexcept>
#include <vector>
using namespace std;
void riempi(vector<int>& v, bool guasto)
{
for (int i = 0; i < 3; ++i) v.push_back(i);
if (guasto) throw runtime_error("riempimento fallito");
}
// versione 1 (basic guarantee): try/catch che libera e rilancia; il chiamante deve fare delete
vector<int>* make_vec1(bool guasto)
{
vector<int>* p = new vector<int>;
try {
riempi(*p, guasto);
} catch (...) {
delete p;
throw;
}
return p;
}
// versione 2: unique_ptr, nessun try/catch e nessuna delete nel chiamante
unique_ptr<vector<int>> make_vec2(bool guasto)
{
unique_ptr<vector<int>> p{new vector<int>};
riempi(*p, guasto); // se lancia, ~unique_ptr elimina il vector
return p; // move: la proprieta' passa al chiamante
}
int main()
{
vector<int>* a = make_vec1(false);
cout << "make_vec1: " << a->size() << " elementi\n";
delete a; // responsabilita' del chiamante
auto b = make_vec2(false); // niente delete: ci pensa unique_ptr
cout << "make_vec2: " << b->size() << " elementi\n";
try { make_vec2(true); } catch (const runtime_error& e) { cout << "errore: " << e.what() << '\n'; }
}- Codice (garanzie/garanzie.cpp):
#include <iostream>
#include <new>
#include <utility>
using namespace std;
class Buffer { // array dinamico di int
public:
static bool guasto; // simula il fallimento dell'allocazione
explicit Buffer(int n, int v = 0) : n_{n}, d_{alloc(n)} { for (int i = 0; i < n_; ++i) d_[i] = v; }
Buffer(const Buffer& b) : n_{b.n_}, d_{alloc(b.n_)} { for (int i = 0; i < n_; ++i) d_[i] = b.d_[i]; }
~Buffer() { delete[] d_; }
// Assegnamento STRONG: prima si fa tutto cio' che puo' fallire su una copia di lavoro,
// poi si scambiano i puntatori con operazioni che non lanciano (commit).
Buffer& operator=(const Buffer& b)
{
Buffer copia{b}; // puo' lanciare: *this e' ancora intatto
swap(copia); // no-throw: commit
return *this; // ~Buffer di 'copia' libera i vecchi dati
}
void swap(Buffer& altro) noexcept { std::swap(n_, altro.n_); std::swap(d_, altro.d_); }
int size() const { return n_; }
int operator[](int i) const { return d_[i]; }
private:
static int* alloc(int n) { if (guasto) throw bad_alloc{}; return new int[n]; }
int n_;
int* d_;
};
bool Buffer::guasto = false;
int main()
{
Buffer a(3, 1), b(5, 2);
Buffer::guasto = true; // da qui ogni new fallisce
try {
a = b; // fallisce nella copia di lavoro
cout << "non arrivo qui\n";
} catch (const bad_alloc&) {
cout << "assegnamento fallito\n";
}
Buffer::guasto = false;
cout << "a: size=" << a.size() << " a[0]=" << a[0] << '\n'; // a e' esattamente com'era
a = b; // ora riesce
cout << "a: size=" << a.size() << " a[0]=" << a[0] << '\n';
}Esercizi su questo argomento
- Esercizio - ConvertToArray da un container STL (secondo appello 2023-24, programmazione)
- Esercizio - Copiare da array a array dinamico senza delete (terzo appello 2022-23, programmazione)
- Esercizio - InertialDriver con buffer circolare (prova intermedia 2025-26)
- Esercizio - Quiz e domande su polimorfismo, allocazione dinamica e garanzie sulle eccezioni (secondo appello 2023-24, teoria)
- Esercizio - RAII e garanzie sulle eccezioni (quarto appello 2021-22, teoria)
- Esercizio - RAII e garanzie sulle eccezioni (terzo appello 2022-23, teoria)
- Esercizio - Smart pointer, unique_ptr e shared_ptr (primo appello 2021-22, teoria)
- Esercizio - Spostare i valori di una list in un array dinamico senza delete (quarto appello 2021-22, programmazione)
- Esercizio - Un unique_ptr minimo e make_unique (laboratorio 10)