Vai al contenuto

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à.

raii_file_descriptor.cpp
#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.

exception_safety.cpp
#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);
Insidia

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 scope

2. 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.

Buona pratica

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.

Concetti chiave
  • 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?