Menu
Coddy logo textTech

RAII come pattern

Fa parte della sezione Programmazione orientata agli oggetti del percorso C++ di Coddy. Lezione 102 di 104.

RAII (Resource Acquisition Is Initialization) è più di un semplice idioma C++: è un potente schema di progettazione che lega la gestione delle risorse al ciclo di vita degli oggetti. Hai già visto RAII con i puntatori intelligenti, ma questo schema si applica a qualsiasi risorsa: handle di file, connessioni di rete, mutex o transazioni del database.

L'idea di base è semplice: acquisisci le risorse nel costruttore e rilasciale nel distruttore. Poiché C++ garantisce che i distruttori vengano eseguiti quando gli oggetti escono dall'ambito, la pulizia avviene automaticamente, anche quando si verificano eccezioni:

#include <iostream>
#include <fstream>

class FileGuard {
    std::ofstream file;
public:
    FileGuard(const std::string& filename) : file(filename) {
        if (!file.is_open()) {
            std::cout << "Failed to open file\n";
        }
    }
    
    void write(const std::string& text) {
        if (file.is_open()) file << text;
    }
    
    ~FileGuard() {
        if (file.is_open()) {
            file.close();
            std::cout << "File closed automatically\n";
        }
    }
};

int main() {
    {
        FileGuard guard("output.txt");
        guard.write("Hello RAII");
    }  // Distruttore chiamato qui - file chiuso
    
    std::cout << "After scope\n";
}

RAII è particolarmente utile nella gestione dei lock nel codice multithread. std::lock_guard della libreria standard segue questo schema: acquisisce un mutex alla costruzione e lo rilascia alla distruzione, evitando i deadlock causati dal mancato rilascio dei lock.

Quando implementi classi RAII, ricordati di eliminare o implementare correttamente le operazioni di copia/spostamento (Regola del Cinque) per prevenire la duplicazione delle risorse o problemi di doppio rilascio. RAII trasforma una gestione manuale delle risorse soggetta a errori in una pulizia sicura e automatica.

challenge icon

Sfida

Facile

Creiamo un Connection Pool Manager usando RAII per gestire in sicurezza le connessioni al database. Nelle applicazioni reali, le connessioni al database sono risorse costose che devono essere acquisite e rilasciate correttamente. Creerai un wrapper RAII che garantisce che le connessioni vengano sempre restituite al pool, anche se si verificano eccezioni o i percorsi del codice diventano complicati.

Organizzerai il codice in tre file:

  • ConnectionPool.h: crea un semplice pool di connessioni che gestisce un numero limitato di connessioni.

    La classe ConnectionPool dovrebbe tenere traccia del numero di connessioni disponibili (inizializzato con una capacità passata al costruttore). Implementa:

    • acquire(): se è disponibile una connessione, diminuisce il conteggio e stampa Connection acquired (X available) dove X è il conteggio rimanente; restituisce true se l'operazione riesce, false se non sono disponibili connessioni
    • release(): aumenta il conteggio delle connessioni disponibili e stampa Connection released (X available)
    • available(): restituisce il numero attuale di connessioni disponibili
  • ConnectionGuard.h: crea il wrapper RAII che gestisce in sicurezza una singola connessione.

    La classe ConnectionGuard rappresenta il pattern RAII. Dovrebbe:

    • Ricevere un riferimento a un ConnectionPool nel costruttore e tentare di acquisire una connessione
    • Memorizzare se l'acquisizione è riuscita
    • Fornire un metodo isConnected() per verificare se il guard possiede una connessione valida
    • Rilasciare automaticamente la connessione al pool nel distruttore (solo se ne è stata acquisita una)
    • Eliminare il costruttore di copia e l'operatore di assegnazione per copia per impedire la duplicazione delle risorse (considerazione relativa alla Rule of Five)

    Quando viene eseguito il distruttore, se è stata mantenuta una connessione, stampa Guard releasing connection prima di chiamare release sul pool.

  • main.cpp: mostra la pulizia automatica di RAII tramite gli ambiti.

    Leggi due input:

    1. Capacità del pool (intero)
    2. Numero di connessioni da richiedere (intero)

    Crea un ConnectionPool con la capacità specificata. Poi, all'interno di un ambito annidato (usando parentesi graffe), crea il numero richiesto di oggetti ConnectionGuard memorizzati in un vector. Per ogni guard, stampa se la connessione è stata stabilita correttamente:

    • Se la connessione è stata stabilita: Guard N: Connected
    • Se la connessione non è stata stabilita: Guard N: Failed to connect

    (dove N parte da 1)

    Al termine dell'ambito (quando i guard vengono distrutti), stampa After scope: X connections available mostrando lo stato finale del pool.

Per esempio, con gli input 2 e 3:

Connection acquired (1 available)
Guard 1: Connected
Connection acquired (0 available)
Guard 2: Connected
Guard 3: Failed to connect
Guard releasing connection
Connection released (1 available)
Guard releasing connection
Connection released (2 available)
After scope: 2 connections available

Con gli input 3 e 2:

Connection acquired (2 available)
Guard 1: Connected
Connection acquired (1 available)
Guard 2: Connected
Guard releasing connection
Connection released (2 available)
Guard releasing connection
Connection released (3 available)
After scope: 3 connections available

Nota come le connessioni vengano rilasciate automaticamente quando i guard escono dal proprio ambito. Non chiami mai esplicitamente release nel codice principale. I distruttori vengono eseguiti in ordine inverso rispetto alla costruzione (l'ultimo guard creato viene distrutto per primo) e ogni connessione acquisita viene sicuramente restituita. Questa è la potenza di RAII: la pulizia delle risorse avviene automaticamente e in modo affidabile, indipendentemente da come termina l'ambito.

Provalo tu

#include <iostream>
#include <vector>
#include "ConnectionPool.h"
#include "ConnectionGuard.h"

using namespace std;

int main() {
    // Leggi gli input
    int capacity;
    int numConnections;
    cin >> capacity;
    cin >> numConnections;
    
    // TODO: Crea un ConnectionPool con la capacità indicata
    
    // TODO: Crea un ambito annidato usando le parentesi graffe
    {
        // TODO: Crea un vector per memorizzare gli oggetti ConnectionGuard
        // Suggerimento: dovrai usare puntatori o puntatori intelligenti perché ConnectionGuard
        // ha il costruttore di copia eliminato
        
        // TODO: Usa un ciclo per creare numConnections guardie
        // Per ogni guardia, stampa una delle due stringhe:
        // "Guard N: Connected" oppure "Guard N: Failed to connect"
        // dove N parte da 1
        
    }
    // Le guardie vengono distrutte qui al termine dell'ambito
    
    // TODO: Stampa "After scope: X connections available"
    
    return 0;
}
quiz iconMettiti alla prova

Questa lezione include un breve quiz. Inizia la lezione per rispondere e tenere traccia dei tuoi progressi.

Tutte le lezioni di Programmazione orientata agli oggetti

Esercitati da solo: Compilatore C++ online