Menu
Coddy logo textTech

RAII как паттерн

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

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

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

#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 стандартной библиотеки следует этому шаблону: он получает мьютекс при создании и освобождает его при уничтожении, предотвращая взаимные блокировки из-за забытых вызовов unlock.

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

challenge icon

Задание

Легко

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

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

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

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

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

    Ваш ConnectionGuard class воплощает шаблон RAII. Он должен:

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

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

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

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

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

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

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

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

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

Например, при входных значениях 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 уничтожается первым), и каждое acquired соединение гарантированно возвращается. В этом заключается сила 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 guards
        // Для каждого guard выведите либо:
        // "Guard N: Connected" or "Guard N: Failed to connect"
        // где N начинается с 1
        
    }
    // Guards уничтожаются здесь, когда область видимости заканчивается
    
    // TODO: Print "After scope: X connections available"
    
    return 0;
}
quiz iconПроверьте себя

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

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

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