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.
Sfida
FacileCreiamo 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
ConnectionPooldovrebbe tenere traccia del numero di connessioni disponibili (inizializzato con una capacità passata al costruttore). Implementa:acquire(): se è disponibile una connessione, diminuisce il conteggio e stampaConnection acquired (X available)dove X è il conteggio rimanente; restituiscetruese l'operazione riesce,falsese non sono disponibili connessionirelease(): aumenta il conteggio delle connessioni disponibili e stampaConnection 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
ConnectionGuardrappresenta il pattern RAII. Dovrebbe:- Ricevere un riferimento a un
ConnectionPoolnel 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 connectionprima di chiamare release sul pool.- Ricevere un riferimento a un
main.cpp: mostra la pulizia automatica di RAII tramite gli ambiti.Leggi due input:
- Capacità del pool (intero)
- Numero di connessioni da richiedere (intero)
Crea un
ConnectionPoolcon la capacità specificata. Poi, all'interno di un ambito annidato (usando parentesi graffe), crea il numero richiesto di oggettiConnectionGuardmemorizzati 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 availablemostrando 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 availableCon 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 availableNota 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;
}
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
1Fondamenti della programmazione orientata agli oggetti
File esterniBuild e compilazione in C++File header e file sorgenteNamespace e ambitoIntroduzione alla programmazione orientata agli oggetti in C++Classi e oggetti a confrontoIl puntatore 'this'Metodi (funzioni membro)Attributi (membri dati)Fondamenti di costruttori e distruttoriRiepilogo - Calcolatrice semplice4Proprietà delle classi
Membri di istanza e staticiGetter e setterFunzioni membro constParola chiave mutableMetodi e variabili staticiFunzioni e classi friendRiepilogo - Gestore di conti bancari7Ereditarietà
Ereditarietà di baseLivelli di accesso nell’ereditarietàOrdine di chiamata di costruttori e distruttoriRidefinizione dei metodiFunzioni virtuali e VTableEreditarietà multiplaEreditarietà virtualeRiepilogo - Gerarchia dei dipendenti2Gestione della memoria
Memoria Stack vs HeapPuntatori e riferimentiMemoria dinamica (new/delete)Puntatori intelligenti in C++RAII in C++Riepilogo - Gestore di array dinamico5Incapsulamento
Specificatori di accesso in C++Specificatori di accesso in dettaglioOccultamento delle informazioniStruct vs classClassi annidate e interneRiepilogo - Sistema di registrazione degli studenti8Polimorfismo
Polimorfismo a compile time e a runtimeOverload delle funzioniFunzioni virtuali: ripassoFunzioni virtuali pureClassi astratteProgettazione delle interfacce in C++Dynamic casting e RTTIRipasso: calcolatrice di forme11Concetti avanzati di OOP
Composizione vs ereditarietàMixin tramite CRTPIdiom PimplType ErasureEnum class e tipizzazione forteGestione delle eccezioni in OOPGerarchie personalizzate di eccezioni14Pattern di progettazione - Parte 2
Pattern CommandPattern AdapterPattern DecoratorPattern Template MethodPattern StatePattern CompositeRAII come pattern3Costruttori e distruttori
Costruttore predefinitoCostruttore con parametriCostruttore di copiaCostruttore di spostamentoListe di inizializzazione dei costruttoriCostruttori delegantiApprofondimento sui distruttoriRegola del Tre / Cinque / ZeroRipasso - classe String6Sovraccarico degli operatori
Introduzione al sovraccarico degli operatoriSovraccarico degli operatori aritmeticiSovraccarico degli operatori di confrontoOperatori di flussoSovraccarico dell'operatore di assegnazioneSovraccarico degli operatori [] e ()Operatori di conversione di tipoRipasso - Classe Matrix9Template
Template di funzioneTemplate di classeSpecializzazione dei templateTemplate variadiciBasi di SFINAE e dei trait di tipoRiepilogo - Contenitore genericoEsercitati da solo: Compilatore C++ online