Menu
Coddy logo textTech

RAII как паттерн

Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 102 из 104.

RAII (Resource Acquisition Is Initialization) — это больше, чем просто идиома C++, это мощный паттерн проектирования, который связывает управление ресурсами со временем жизни объекта. Вы уже видели RAII на примере умных указателей, но этот паттерн применим к любому ресурсу: дескрипторам файлов, сетевым соединениям, мьютексам или транзакциям баз данных.

Основная идея проста: захватывайте ресурсы в конструкторе, освобождайте их в деструкторе. Поскольку C++ гарантирует выполнение деструкторов, когда объекты выходят из области видимости, очистка происходит автоматически — даже при возникновении исключений:

#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");
    }  // Здесь вызывается деструктор - файл закрыт
    
    std::cout << "After scope\n";
}

RAII проявляет себя наилучшим образом при управлении блокировками в многопоточном коде. std::lock_guard из стандартной библиотеки следует этому паттерну — он захватывает мьютекс при создании и освобождает его при уничтожении, предотвращая взаимные блокировки из-за забытых разблокировок.

При реализации RAII-классов не забывайте либо удалять, либо правильно реализовывать операции копирования/перемещения (правило пяти), чтобы предотвратить дублирование ресурсов или проблемы с двойным освобождением. RAII превращает подверженное ошибкам ручное управление ресурсами в безопасную автоматическую очистку.

challenge icon

Задание

Легко

Давайте создадим Менеджер пула соединений, используя RAII для безопасного управления соединениями с базой данных. В реальных приложениях соединения с базой данных являются дорогостоящими ресурсами, которые должны быть правильно получены и освобождены. Вы создадите RAII-обертку, которая гарантирует, что соединения всегда возвращаются в пул, даже если возникают исключения или пути выполнения кода усложняются.

Вы организуете свой код в трех файлах:

  • ConnectionPool.h: Создайте простой пул соединений, который управляет ограниченным количеством соединений.

    Ваш класс ConnectionPool должен отслеживать количество доступных соединений (начиная с емкости, переданной в конструктор). Реализуйте:

    • acquire() — если соединение доступно, уменьшите счетчик и выведите "Connection acquired (X available)", где X — оставшееся количество; верните true в случае успеха, false, если соединений нет
    • release() — увеличьте количество доступных соединений и выведите "Connection released (X available)"
    • available() — возвращает текущее количество доступных соединений
  • ConnectionGuard.h: Создайте RAII-обертку, которая безопасно управляет одним соединением.

    Ваш класс ConnectionGuard воплощает паттерн RAII. Он должен:

    • Принимать ссылку на ConnectionPool в своем конструкторе и пытаться получить соединение
    • Хранить информацию о том, было ли получение успешным
    • Предоставлять метод isConnected() для проверки того, удерживает ли guard активное соединение
    • Автоматически возвращать соединение в пул в деструкторе (только если оно было получено)
    • Удалить конструктор копирования и оператор присваивания копированием для предотвращения дублирования ресурсов (согласно Правилу пяти)

    Когда запускается деструктор, если соединение удерживалось, выведите "Guard releasing connection" перед вызовом release в пуле.

  • main.cpp: Продемонстрируйте автоматическую очистку RAII через области видимости.

    Считайте два входных значения:

    1. Емкость пула (целое число)
    2. Количество запрашиваемых соединений (целое число)

    Создайте ConnectionPool с заданной емкостью. Затем внутри вложенной области видимости (используя фигурные скобки) создайте запрошенное количество объектов ConnectionGuard, хранящихся в векторе. Для каждого guard выведите, успешно ли он подключился:

    • Если подключен: "Guard N: Connected"
    • Если не подключен: "Guard N: Failed to connect"

    (где N начинается с 1)

    После завершения области видимости (когда объекты guard уничтожены) выведите "After scope: X connections available", показывая конечное состояние пула.

Например, при входных данных 2 и 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

При входных данных 3 и 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

Обратите внимание, как соединения автоматически освобождаются, когда объекты guard выходят из области видимости — вы никогда не вызываете release явно в основном коде. Деструкторы запускаются в порядке, обратном порядку создания (последний созданный guard уничтожается первым), и каждое полученное соединение гарантированно возвращается. В этом и заключается сила RAII: очистка ресурсов происходит автоматически и надежно, независимо от того, как завершается область видимости.

Шпаргалка

RAII (Resource Acquisition Is Initialization — «Получение ресурса есть инициализация») — это паттерн проектирования, который связывает управление ресурсами со временем жизни объекта. Ресурсы захватываются в конструкторе и освобождаются в деструкторе.

Поскольку C++ гарантирует выполнение деструкторов при выходе объектов из области видимости, очистка происходит автоматически — даже при возникновении исключений:

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");
    }  // Здесь вызывается деструктор — файл закрывается автоматически
    
    std::cout << "After scope\n";
}

RAII особенно полезен для управления блокировками в многопоточном коде. std::lock_guard из стандартной библиотеки захватывает мьютекс при создании и освобождает его при уничтожении, предотвращая взаимные блокировки (deadlocks) из-за забытых разблокировок.

При реализации RAII-классов следует удалять или правильно реализовывать операции копирования/перемещения (правило пяти), чтобы предотвратить дублирование ресурсов или проблемы двойного освобождения.

Попробуйте сами

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

using namespace std;

int main() {
    // Чтение входных данных
    int capacity;
    int numConnections;
    cin >> capacity;
    cin >> numConnections;
    
    // TODO: Создайте ConnectionPool с заданной емкостью capacity
    
    // TODO: Создайте вложенную область видимости, используя фигурные скобки
    {
        // TODO: Создайте вектор для хранения объектов ConnectionGuard
        // Подсказка: вам нужно будет использовать указатели или умные указатели, так как у ConnectionGuard
        // удален конструктор копирования
        
        // TODO: Запустите цикл для создания numConnections гардов
        // Для каждого гарда выведите либо:
        // "Guard N: Connected" или "Guard N: Failed to connect"
        // где N начинается с 1
        
    }
    // Гарды уничтожаются здесь, когда заканчивается область видимости
    
    // TODO: Выведите "After scope: X connections available"
    
    return 0;
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование