RAII: il pattern fondamentale per le risorse
Impara il RAII (Resource Acquisition Is Initialization) — il pattern C++ fondamentale per una gestione delle risorse a prova di eccezioni. Implementa wrapper RAII, comprendi lock_guard e scoped_lock e scopri perché i tipi RAII devono essere solo spostabili oppure a conteggio dei riferimenti.
RAII: acquisire nel costruttore, rilasciare nel distruttore
Il RAII (Resource Acquisition Is Initialization) è il pattern più importante in assoluto del C++. L'idea è semplice:
1. Acquisisci una risorsa nel costruttore (apri un file, blocca un mutex, alloca memoria, connettiti a un database).
2. Rilascia la risorsa nel distruttore (chiudi il file, sblocca il mutex, libera la memoria, disconnettiti).
Poiché il C++ garantisce che i distruttori vengano chiamati quando un oggetto esce dallo scope — anche se viene lanciata un'eccezione — il RAII rende la gestione delle risorse automatica e a prova di eccezioni. Non ti dimentichi mai di rilasciare e non rilasci mai due volte.
Ogni tipo C++ ben progettato che gestisce una risorsa usa il RAII: std::string, std::vector, std::unique_ptr, std::fstream, std::lock_guard, std::unique_lock.
Implementare un wrapper RAII
Ecco un wrapper RAII completo per un descrittore di file (POSIX). Acquisisce la risorsa nel costruttore, la rilascia nel distruttore e supporta la semantica di spostamento per il trasferimento della proprietà.
#include <unistd.h> // open, close, read, write
#include <fcntl.h> // O_RDONLY, etc.
#include <stdexcept>
#include <utility>
#include <iostream>
class FileDescriptor {
int fd_ = -1; // -1 = no resource
public:
// Acquire: open file in constructor
explicit FileDescriptor(const char* path, int flags)
: fd_(::open(path, flags)) {
if (fd_ < 0) throw std::runtime_error("Cannot open file");
}
// Release: close file in destructor
~FileDescriptor() {
if (fd_ >= 0) ::close(fd_);
}
// Non-copyable (copying a file descriptor is meaningless)
FileDescriptor(const FileDescriptor&) = delete;
FileDescriptor& operator=(const FileDescriptor&) = delete;
// Moveable (transfer ownership)
FileDescriptor(FileDescriptor&& other) noexcept
: fd_(std::exchange(other.fd_, -1)) {}
FileDescriptor& operator=(FileDescriptor&& other) noexcept {
if (this != &other) {
if (fd_ >= 0) ::close(fd_);
fd_ = std::exchange(other.fd_, -1);
}
return *this;
}
int get() const { return fd_; }
};
int main() {
try {
FileDescriptor fd("/tmp/raii_test.txt", O_RDONLY);
std::cout << "File opened: fd=" << fd.get() << '\n';
// Use fd...
} catch (const std::exception& e) {
std::cerr << e.what() << '\n';
}
// fd.~FileDescriptor() called automatically — file closed
// Even if an exception was thrown, the file is closed
}Sicurezza rispetto alle eccezioni grazie al RAII
Senza il RAII, scrivere codice a prova di eccezioni richiede noiosi blocchi try/catch per ogni risorsa. Con il RAII è automatico — ogni distruttore scatta durante lo srotolamento dello stack.
#include <memory>
#include <mutex>
#include <fstream>
#include <vector>
#include <stdexcept>
std::mutex g_mutex;
// WITHOUT RAII — manual cleanup is error-prone
void bad_example() {
std::mutex* m = &g_mutex;
m->lock();
int* data = new int[100];
// If this throws, mutex stays locked AND data leaks!
// process(data);
delete[] data;
m->unlock();
}
// WITH RAII — automatic, exception-safe cleanup
void good_example() {
std::lock_guard<std::mutex> lock(g_mutex); // RAII lock
auto data = std::make_unique<int[]>(100); // RAII allocation
// If this throws:
// 1. data's destructor frees the memory
// 2. lock's destructor unlocks the mutex
// Stack unwinding handles everything
// process(data.get());
}
// Multiple resources — RAII composes naturally
void multi_resource() {
std::lock_guard<std::mutex> lock(g_mutex);
auto buffer = std::make_unique<char[]>(4096);
std::ofstream file("/tmp/output.txt");
// If any operation throws, ALL resources are released
// in reverse order of construction:
// 1. file closed
// 2. buffer freed
// 3. mutex unlocked
}lock_guard, unique_lock e scoped_lock
La libreria standard fornisce wrapper RAII per i mutex:
- std::lock_guard — il lock RAII più semplice. Blocca nel costruttore, sblocca nel distruttore. Non spostabile e non copiabile. Usalo quando ti serve un semplice lock legato allo scope.
- std::unique_lock — lock RAII flessibile. Supporta il blocco differito, il blocco con timeout, lo sblocco e il riblocco manuali, e può essere spostato. È obbligatorio per l'uso con std::condition_variable.
- std::scoped_lock (C++17) — blocca più mutex contemporaneamente usando un algoritmo che evita i deadlock. È il tipo di lock da preferire quando si bloccano uno o più mutex.
std::mutex m1, m2;
// C++17: blocca entrambi senza rischio di deadlock
std::scoped_lock lock(m1, m2);1. Gli oggetti RAII senza nome vengono distrutti immediatamente:
std::lock_guard<std::mutex>(mtx); // BUG! Temporaneo — blocca e sblocca subito
std::lock_guard<std::mutex> lock(mtx); // Corretto — ha un nome, vive fino a fine scope2. Non copiare ingenuamente i tipi RAII: copiare un handle di file o un lock su un mutex non ha alcun senso. I tipi RAII dovrebbero essere:
- Solo spostabili (come unique_ptr, unique_lock) per la proprietà esclusiva, oppure
- A conteggio dei riferimenti (come shared_ptr) per la proprietà condivisa.
3. Il distruttore non dovrebbe lanciare eccezioni: se un distruttore lancia un'eccezione durante lo srotolamento dello stack (mentre un'altra eccezione è attiva), viene chiamata std::terminate. Da C++11 in poi i distruttori sono noexcept per impostazione predefinita.
1. Ogni risorsa dovrebbe essere posseduta da un oggetto RAII — memoria, file, lock, socket, connessioni a database, handle della GPU.
2. Una risorsa per classe RAII — mantieni i wrapper focalizzati.
3. Elimina le operazioni di copia con delete, a meno che copiare la risorsa non abbia davvero senso.
4. Implementa le operazioni di spostamento per abilitare il trasferimento della proprietà.
5. Usa std::exchange nelle operazioni di spostamento — legge e reimposta la sorgente in un colpo solo: fd_ = std::exchange(other.fd_, -1);
6. Preferisci i tipi RAII standard (unique_ptr, lock_guard, fstream) — scrivi wrapper personalizzati solo per le risorse che non ne hanno uno standard.
Comporre tipi RAII (regola dello zero)
L'espressione più alta del RAII è la regola dello zero: se tutte le variabili membro sono tipi RAII, la tua classe non ha bisogno di alcun membro speciale dichiarato dall'utente. Il distruttore e le operazioni di copia e spostamento generati dal compilatore fanno automaticamente la cosa giusta.
class DatabaseSession {
std::unique_ptr<Connection> conn_; // RAII: chiuso automaticamente
std::unique_lock<std::mutex> lock_; // RAII: sbloccato automaticamente
std::vector<std::string> log_; // RAII: liberato automaticamente
// Non servono distruttore, copia o spostamento — ci pensano i tipi dei membri
};La regola dello zero è l'obiettivo. La regola del cinque (definire esplicitamente tutti e 5 i membri speciali) vale per i tipi wrapper di risorse di basso livello. Non esiste una via di mezzo: se dichiari uno dei cinque, dichiarali tutti.
- RAII: acquisisci nel costruttore, rilascia nel distruttore — automatico e a prova di eccezioni
- I distruttori vengono chiamati durante lo srotolamento dello stack, quindi il RAII gestisce le eccezioni in modo naturale
- I tipi RAII dovrebbero essere solo spostabili (proprietà esclusiva) o a conteggio dei riferimenti (proprietà condivisa)
- Usa
std::scoped_lock(C++17) per bloccare più mutex senza rischio di deadlock - Gli oggetti RAII senza nome vengono distrutti immediatamente — dai loro sempre un nome
- Segui la regola dello zero: usa membri RAII così che il compilatore generi membri speciali corretti
Quiz — Metti alla prova le tue conoscenze
(15 XP)1. Che cosa c'è di sbagliato in `std::lock_guard<std::mutex>(mtx);` (nota: senza nome di variabile)?
2. Perché i tipi RAII dovrebbero tipicamente eliminare le proprie operazioni di copia?
3. Che cos'è la regola dello zero?