Menu
Coddy logo textTech

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.

challenge icon

Wyzwanie

Łatwy

Zbudujmy 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 ConnectionPool powinna ś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 wypisz Connection acquired (X available), gdzie X oznacza pozostałą liczbę; zwróć true, jeśli się powiedzie, a false, jeśli nie ma dostępnych połączeń
    • release(): zwiększ liczbę dostępnych połączeń i wypisz Connection 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 ConnectionGuard realizuje wzorzec RAII. Powinna:

    • Przyjmować referencję do ConnectionPool w 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 connection przed wywołaniem zwalniania połączenia w puli.

  • main.cpp: Zademonstruj automatyczne sprzątanie RAII za pomocą zakresów.

    Wczytaj dwa dane wejściowe:

    1. Pojemność puli (liczba całkowita)
    2. Liczba żądanych połączeń (liczba całkowita)

    Utwórz ConnectionPool o podanej pojemności. Następnie, wewnątrz zagnieżdżonego zakresu (używając nawiasów klamrowych), utwórz żądaną liczbę obiektów ConnectionGuard przechowywanych 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 available

Przy 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 available

Zwróć 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;
}
quiz iconSprawdź się

Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.

Wszystkie lekcje w sekcji Programowanie obiektowe

Poćwicz samodzielnie: Kompilator C++ online