RAII jako wzorzec
Część sekcji Programowanie obiektowe ścieżki C++ w Coddy. Lekcja 102 z 104.
RAII (Resource Acquisition Is Initialization) to coś więcej niż tylko idiom języka C++ — to potężny wzorzec projektowy, który wiąże zarządzanie zasobami z czasem życia obiektu. RAII zostało już omówione przy okazji inteligentnych wskaźników, ale ten wzorzec ma zastosowanie do dowolnych zasobów: uchwytów plików, połączeń sieciowych, muteksów czy transakcji bazodanowych.
Główna idea jest prosta: pozyskuj zasoby w konstruktorze, zwalniaj je w destruktorze. Ponieważ C++ gwarantuje, że destruktory są uruchamiane, gdy obiekty wychodzą poza zakres, sprzątanie odbywa się automatycznie — nawet w przypadku wyjątków:
#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");
} // Destruktor wywoływany tutaj - plik zamknięty
std::cout << "After scope\n";
}RAII sprawdza się doskonale przy zarządzaniu blokadami w kodzie wielowątkowym. std::lock_guard z biblioteki standardowej stosuje ten wzorzec — blokuje mutex podczas tworzenia i zwalnia go podczas niszczenia, zapobiegając zakleszczeniom wynikającym z zapomnienia o odblokowaniu.
Podczas implementowania klas RAII pamiętaj, aby usunąć operacje kopiowania/przenoszenia lub poprawnie je zaimplementować (reguła pięciu), by zapobiec duplikowaniu zasobów lub problemom z ich podwójnym zwalnianiem. RAII przekształca podatne na błędy ręczne zarządzanie zasobami w bezpieczne, automatyczne sprzątanie.
Wyzwanie
ŁatwyZbudujmy menedżer puli połączeń używający RAII do bezpiecznego zarządzania połączeniami z bazą danych. W rzeczywistych aplikacjach połączenia z bazą danych są kosztownymi zasobami, które należy prawidłowo uzyskiwać i zwalniać. Utworzysz opakowanie RAII, które gwarantuje, że połączenia są zawsze zwracane do puli, nawet jeśli wystąpią wyjątki lub ścieżki wykonania kodu staną się skomplikowane.
Rozmieścisz kod w trzech plikach:
ConnectionPool.h: Utwórz prostą pulę połączeń, która zarządza ograniczoną liczbą połączeń.Twoja klasa
ConnectionPoolpowinna śledzić liczbę dostępnych połączeń (rozpocznij od pojemności przekazanej do konstruktora). Zaimplementuj:acquire(): jeśli połączenie jest dostępne, zmniejsz licznik i wypiszConnection acquired (X available), gdzie X oznacza pozostałą liczbę; zwróćtrue, jeśli się powiedzie, afalse, jeśli nie ma dostępnych połączeńrelease(): zwiększ liczbę dostępnych połączeń i wypiszConnection released (X available)available(): zwraca bieżącą liczbę dostępnych połączeń
ConnectionGuard.h: Zbuduj opakowanie RAII, które bezpiecznie zarządza pojedynczym połączeniem.Twoja klasa
ConnectionGuardrealizuje wzorzec RAII. Powinna:- Przyjmować referencję do
ConnectionPoolw konstruktorze i próbować uzyskać połączenie - Zapamiętywać, czy uzyskanie połączenia się powiodło
- Udostępniać metodę
isConnected(), która sprawdza, czy strażnik ma prawidłowe połączenie - Automatycznie zwalniać połączenie z powrotem do puli w destruktorze (tylko jeśli zostało uzyskane)
- Usuwać konstruktor kopiujący i operator przypisania kopiującego, aby zapobiec duplikowaniu zasobów (kwestia reguły pięciu)
Gdy uruchomi się destruktor, jeśli połączenie było zajęte, wypisz
Guard releasing connectionprzed wywołaniem zwalniania połączenia w puli.- Przyjmować referencję do
main.cpp: Zademonstruj automatyczne sprzątanie RAII za pomocą zakresów.Wczytaj dwa dane wejściowe:
- Pojemność puli (liczba całkowita)
- Liczba żądanych połączeń (liczba całkowita)
Utwórz
ConnectionPoolo podanej pojemności. Następnie, wewnątrz zagnieżdżonego zakresu (używając nawiasów klamrowych), utwórz żądaną liczbę obiektówConnectionGuardprzechowywanych w tablicy. Dla każdego strażnika wypisz, czy udało mu się połączyć:- Jeśli połączenie zostało uzyskane:
Guard N: Connected - Jeśli połączenie nie zostało uzyskane:
Guard N: Failed to connect
(gdzie N zaczyna się od 1)
Po zakończeniu zakresu (zniszczeniu strażników) wypisz
After scope: X connections available, pokazując końcowy stan puli.
Na przykład, przy danych wejściowych 2 i 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 availablePrzy danych wejściowych 3 i 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 availableZwróć uwagę, że połączenia są automatycznie zwalniane, gdy strażniki wychodzą poza zakres. Nigdy nie wywołujesz jawnie release w swoim kodzie głównym. Destruktory uruchamiają się w odwrotnej kolejności do tworzenia obiektów (najpierw niszczony jest ostatni strażnik), a każde uzyskane połączenie z pewnością zostanie zwrócone. Na tym polega siła RAII: sprzątanie zasobów odbywa się automatycznie i niezawodnie, niezależnie od tego, jak kończy się zakres.
Spróbuj swoich sił
#include <iostream>
#include <vector>
#include "ConnectionPool.h"
#include "ConnectionGuard.h"
using namespace std;
int main() {
// Wczytaj dane wejściowe
int capacity;
int numConnections;
cin >> capacity;
cin >> numConnections;
// TODO: Utwórz ConnectionPool o podanej pojemności
// TODO: Utwórz zagnieżdżony zakres za pomocą nawiasów klamrowych
{
// TODO: Utwórz wektor do przechowywania obiektów ConnectionGuard
// Wskazówka: Musisz użyć wskaźników lub inteligentnych wskaźników, ponieważ ConnectionGuard
// ma usunięty konstruktor kopiujący
// TODO: Użyj pętli, aby utworzyć numConnections obiektów ConnectionGuard
// Dla każdego obiektu ConnectionGuard wypisz jedną z wartości:
// "Guard N: Connected" lub "Guard N: Failed to connect"
// gdzie N zaczyna się od 1
}
// Obiekty ConnectionGuard są tu niszczone po zakończeniu zakresu
// TODO: Wypisz "After scope: X connections available"
return 0;
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Podstawy programowania obiektowego
Pliki zewnętrzneBudowanie i kompilacja C++Pliki nagłówkowe i pliki źródłowePrzestrzenie nazw i zakresWprowadzenie do programowania obiektowego w C++Klasy a obiektyWskaźnik „this”Metody (funkcje składowe)Atrybuty (składowe danych)Podstawy konstruktorów i destruktorówPowtórzenie – prosty kalkulator4Właściwości klas
Elementy instancji a elementy statyczneGettery i setteryStałe funkcje składoweSłowo kluczowe mutableMetody i zmienne statyczneFunkcje i klasy zaprzyjaźnionePodsumowanie – menedżer konta bankowego7Dziedziczenie
Podstawy dziedziczeniaPoziomy dostępu w dziedziczeniuKolejność wywołań konstruktora i destruktoraNadpisywanie metodFunkcje wirtualne i VTableWielokrotne dziedziczenieDziedziczenie wirtualnePowtórzenie — hierarchia pracowników2Zarządzanie pamięcią
Pamięć stosu a stertyWskaźniki i referencjePamięć dynamiczna (new/delete)Inteligentne wskaźniki w C++RAII w C++Podsumowanie — menedżer tablicy dynamicznej5Hermetyzacja
Specyfikatory dostępu w C++Specyfikatory dostępu — szczegółowoUkrywanie informacjiStruktura a klasaKlasy zagnieżdżone i wewnętrznePodsumowanie — system ewidencji studentów8Polimorfizm
Polimorfizm czasu kompilacji i wykonaniaPrzeciążanie funkcjiFunkcje wirtualne — powtórkaCzysto wirtualne funkcjeKlasy abstrakcyjneProjektowanie interfejsów w C++Rzutowanie dynamiczne i RTTIPodsumowanie — kalkulator kształtów11Zaawansowane koncepcje OOP
Kompozycja a dziedziczenieMixiny z użyciem CRTPIdiom PimplWymazywanie typówKlasy enum i silne typowanieObsługa wyjątków w OOPWłasne hierarchie wyjątków14Wzorce projektowe, część 2
Wzorzec poleceniaWzorzec adapteraWzorzec dekoratoraWzorzec metody szablonowejWzorzec stanuWzorzec kompozytuRAII jako wzorzec3Konstruktory i destruktory
Konstruktor domyślnyKonstruktor z parametramiKonstruktor kopiującyKonstruktor przenoszącyListy inicjalizacyjne konstruktoraKonstruktory delegująceDestruktor — szczegółowe omówienieZasada trzech / pięciu / zeraPodsumowanie — klasa String6Przeciążanie operatorów
Wprowadzenie do przeciążania operatorówPrzeciążanie operatorów arytmetycznychPrzeciążanie operatorów porównaniaOperatory strumieniowePrzeciążanie operatora przypisaniaPrzeciążanie operatorów [] i ()Operatory konwersji typówPodsumowanie — klasa Matrix9Szablony
Szablony funkcjiSzablony klasSpecjalizacja szablonówSzablony wariadycznePodstawy SFINAE i cech typówPodsumowanie — kontener generycznyPoćwicz samodzielnie: Kompilator C++ online