Menu
Coddy logo textTech

RAII как паттерн

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

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

Основная идея проста: acquire ресурсы в constructor, освобождайте их в деструкторе. Поскольку 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 помни: нужно либо delete, либо корректно реализовать операции copy/move (Правило Five), чтобы предотвратить дублирование ресурсов или проблемы с двойным освобождением. RAII превращает подверженное ошибкам ручное управление ресурсами в безопасную автоматическую очистку.

challenge icon

Задание

Легко

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

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

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

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

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

    Ваш ConnectionGuard class реализует шаблон RAII. Он должен:

    • Принимать ссылку на ConnectionPool в своём constructor и пытаться получить подключение
    • Хранить информацию о том, успешно ли прошло получение подключения
    • Предоставлять метод isConnected() для проверки того, удерживает ли guard действительное подключение
    • automatically возвращать подключение в пул в destructor (только если оно было получено)
    • Удалить copy constructor и copy assignment, чтобы предотвратить дублирование ресурса (учёт Rule of Five)

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

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

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

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

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

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

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

    После завершения области видимости (guards уничтожены) выведите 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

Обратите внимание, как подключения automatically освобождаются, когда guards выходят из области видимости. В основном коде вам никогда не нужно явно вызывать release. Destructors выполняются в обратном порядке создания (последний guard уничтожается первым), и каждое полученное подключение гарантированно возвращается. В этом и заключается сила 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 с заданной ёмкостью
    
    // TODO: Создайте вложенную область видимости с помощью фигурных скобок
    {
        // TODO: Создайте vector для хранения объектов ConnectionGuard
        // Подсказка: Вам понадобятся указатели или умные указатели, поскольку ConnectionGuard
        // имеет удалённый конструктор копирования
        
        // TODO: Цикл для создания numConnections гвардов
        // Для каждого гварда выведите либо:
        // "Guard N: Connected" or "Guard N: Failed to connect"
        // где N начинается с 1
        
    }
    // Гварды уничтожаются здесь, когда область видимости заканчивается
    
    // TODO: Print "After scope: X connections available"
    
    return 0;
}
quiz iconПроверьте себя

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

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

Потренируйтесь самостоятельно: Онлайн-компилятор C++