Esercizio - RAII e garanzie sulle eccezioni (terzo appello 2022-23, teoria)
Domanda aperta 3 del terzo appello 2022-23 del corso di Laboratorio di Programmazione, Ing. Informatica UniPD. La stessa domanda era nel quarto appello 2021-22 (Esercizio - RAII e garanzie sulle eccezioni (quarto appello 2021-22, teoria)), svolta con l'esempio di make_vec e con la gestione della memoria. Qui lo stesso argomento è illustrato con un'altra risorsa (un lock) e con un esempio della differenza fra le garanzie. Teoria: 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) →, C++ - smart pointerGli smart pointer (classi template di
Testo. Descrivere la tecnica RAII: cosa significa e come viene utilizzata nei costruttori e distruttori di una classe costruiti opportunamente. Descrivere i concetti di basic guarantee, strong guarantee, no-throw guarantee.
RAII: che cosa significa
RAII = Resource Acquisition Is Initialization: l'acquisizione di una risorsa (memoria, file aperto, lock, thread, socket, finestra) coincide con l'inizializzazione di un oggetto. Si acquisisce nel costruttore e si rilascia nel distruttore.
Perché funziona: in C++ il distruttore di un oggetto locale è chiamato automaticamente quando il flusso di esecuzione lascia lo scope, in qualunque modo lo lasci: fine normale del blocco, return anticipato, break, eccezione. Il codice di rilascio sta in un solo posto e non può essere "saltato". In più:
- l'oggetto esiste solo se l'acquisizione è riuscita: se il costruttore lancia, l'oggetto non esiste e non c'è nulla da rilasciare;
- ci si libera di
try/catch (...) { rilascia; throw; }ripetuti dappertutto; - gli oggetti composti funzionano da soli: il distruttore di una classe distrugge i sotto-oggetti.
Come si scrive la classe: costruttore = acquisisce (e se non riesce lancia); distruttore = rilascia tutto, senza lanciare; copia: vietata (= delete) o profonda; spostamento: trasferisce la risorsa. Gli smart pointer e vector sono classi RAII.
#include <iostream>
using namespace std;
// Classe RAII: acquisisce il "lock" nel costruttore e lo rilascia nel distruttore
class Lock {
public:
explicit Lock(const char* nome) : nome_{nome} { cout << " lock(" << nome_ << ")\n"; }
~Lock() { cout << " unlock(" << nome_ << ")\n"; }
Lock(const Lock&) = delete; // un lock non si copia
Lock& operator=(const Lock&) = delete;
private:
const char* nome_;
};
int funzione(int x)
{
Lock a("A"); // acquisito qui
if (x == 0) {
cout << " uscita anticipata (x == 0)\n";
return -1; // il distruttore di a e' chiamato comunque
}
{
Lock b("B"); // blocco interno
cout << " lavoro dentro il blocco interno\n";
} // unlock(B) alla chiusura del blocco
cout << " fine normale\n";
return x; // unlock(A) all'uscita
}
int main()
{
cout << "funzione(5):\n";
funzione(5);
cout << "funzione(0):\n";
funzione(0);
return 0;
}funzione(5):
lock(A)
lock(B)
lavoro dentro il blocco interno
unlock(B)
fine normale
unlock(A)
funzione(0):
lock(A)
uscita anticipata (x == 0)
unlock(A)La classe Lock acquisisce in lock(...) (il costruttore) e rilascia in unlock(...) (il distruttore). In funzione(5): A si rilascia in uscita, B alla chiusura del blocco interno. In funzione(0): il return anticipato salta tutto il resto, ma il distruttore di a è chiamato comunque: unlock(A) compare. Con un'eccezione lanciata dentro la funzione succederebbe lo stesso (stack unwinding): senza RAII sarebbe necessario un try/catch con unlock e throw;. Un lock che resta preso per una dimenticanza è un caso classico di deadlock.
Le tre garanzie
Quando una funzione può lanciare un'eccezione, si descrive che cosa si può contare che sia vero dopo il fallimento:
| garanzia | promessa |
|---|---|
| basic guarantee | se la funzione lancia, non c'è nessun leak di risorse e gli oggetti restano in uno stato valido (invarianti rispettati), ma lo stato può essere cambiato rispetto a prima |
| strong guarantee | la basic più: in caso di fallimento tutti i valori osservabili (non locali alla funzione) sono quelli di prima della chiamata: commit or rollback, tutto o niente |
| no-throw guarantee | la funzione non lancia mai (distruttori, swap, funzioni noexcept) |
La STL fornisce la basic guarantee per tutte le sue operazioni. La strong guarantee si ottiene tipicamente con il metodo copia, modifica, scambia: si lavora su una copia (se qualcosa lancia, l'originale non è stato toccato), poi si scambia con l'originale con un'operazione no-throw (swap): il commit.
#include <iostream>
#include <vector>
using namespace std;
struct Fallisce {}; // tipo che segnala il fallimento
void operazione_rischiosa(int i, int fallisci_a) // lancia quando i == fallisci_a
{
if (i == fallisci_a) throw Fallisce();
}
// BASIC GUARANTEE: se lancia, nessun leak e v e' valido, ma puo' essere stato modificato a meta'
void aggiungi_basic(vector<int>& v, int n, int fallisci_a)
{
for (int i = 0; i < n; ++i) {
operazione_rischiosa(i, fallisci_a);
v.push_back(i * 10); // ogni push_back e' gia' "committato"
}
}
// STRONG GUARANTEE (commit or rollback): se lancia, v e' identico a prima della chiamata
void aggiungi_strong(vector<int>& v, int n, int fallisci_a)
{
vector<int> copia = v; // si lavora su una COPIA
for (int i = 0; i < n; ++i) {
operazione_rischiosa(i, fallisci_a);
copia.push_back(i * 10);
}
v.swap(copia); // commit: lo swap non lancia (no-throw)
}
void stampa(const char* nome, const vector<int>& v)
{
cout << nome << " (size " << v.size() << "):";
for (int x : v) cout << ' ' << x;
cout << '\n';
}
int main()
{
vector<int> v1 {1, 2};
try { aggiungi_basic(v1, 5, 3); }
catch (Fallisce&) { cout << "basic: eccezione al passo 3\n"; }
stampa("v1 dopo basic ", v1);
vector<int> v2 {1, 2};
try { aggiungi_strong(v2, 5, 3); }
catch (Fallisce&) { cout << "strong: eccezione al passo 3\n"; }
stampa("v2 dopo strong", v2);
vector<int> v3 {1, 2};
aggiungi_strong(v3, 5, -1); // senza fallimenti: il commit avviene
stampa("v3 senza errori", v3);
return 0;
}Uscita (il programma usa try/catch: in WebAssembly le eccezioni non funzionano, quindi è compilato per Linux e l'uscita è ricavata a mano dalle regole del linguaggio):
basic: eccezione al passo 3
v1 dopo basic (size 5): 1 2 0 10 20
strong: eccezione al passo 3
v2 dopo strong (size 2): 1 2
v3 senza errori (size 7): 1 2 0 10 20 30 40aggiungi_basic: aggiunge elementi uno alla volta; quando al passo 3 lancia,v1contiene già (valido ma cambiato: basic guarantee, nessun leak).aggiungi_strong: lavora sucopia; al passo 3 lancia prima delloswap, quindiv2è identico a prima (1 2): strong guarantee. Il commit (v.swap(copia)) è no-throw. Senza errori (v3) lo scambio avviene ev3ha tutti gli elementi.- Il prezzo della strong è una copia in più.
Versione ripasso
- RAII (Resource Acquisition Is Initialization): risorsa (memoria, file, lock, thread, socket) acquisita nel costruttore, rilasciata nel distruttore. Il distruttore dei locali è chiamato in qualunque modo si lasci lo scope (fine blocco,
return,break, eccezione: stack unwinding): il rilascio sta in un posto solo; se il costruttore lancia l'oggetto non esiste; nientetry/catch (...) { rilascia; throw; }ovunque. Classe: costruttore acquisisce (lancia se fallisce), distruttore rilascia tutto senza lanciare, copia vietata o profonda. Rimando: 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) →.
class Lock { public: explicit Lock(const char* n) : nome_{n} { /* lock */ } ~Lock() { /* unlock */ }
Lock(const Lock&) = delete; Lock& operator=(const Lock&) = delete; private: const char* nome_; };
int funzione(int x) { Lock a("A"); if (x == 0) return -1; /* unlock(A) comunque */ /* ... */ return x; } Prova: funzione(0) stampa lock(A), uscita anticipata, unlock(A); funzione(5): lock(A), lock(B), unlock(B) alla chiusura del blocco, unlock(A) in uscita.
| garanzia | promessa |
|---|---|
| basic | se lancia: nessun leak, stato valido, ma forse cambiato |
| strong | basic + valori osservabili come prima (commit or rollback) |
| no-throw | non lancia mai (distruttori, swap, noexcept) |
- Strong con copia, modifica, scambia:
vector<int> copia = v; /* modifiche su copia, possono lanciare */ v.swap(copia);(commit no-throw). Esempio:aggiungi_basicche lancia al passo 3 lasciav1 = 1 2 0 10 20(valido, cambiato);aggiungi_stronglasciav2 = 1 2(invariato); senza erroriv3 = 1 2 0 10 20 30 40. La STL dà la basic. - Errori tipici:
catchche non rilancia; distruttore che lancia; confondere basic e strong (la strong richiede lo stato invariato); acquisire la risorsa fuori dal costruttore; credere che servatry/catchcon gli smart pointer.