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 превращает подверженное ошибкам ручное управление ресурсами в безопасную автоматическую очистку.
Задание
ЛегкоДавайте создадим Connection Pool Manager с использованием RAII для безопасного управления подключениями к базе данных. В реальных приложениях подключения к базе данных являются дорогостоящими ресурсами, которые необходимо правильно получать и освобождать. Вы создадите оболочку RAII, гарантирующую, что подключения всегда возвращаются в пул, даже если возникают исключения или пути выполнения кода становятся сложными.
Вы организуете код в трёх файлах:
ConnectionPool.h: Создайте простой пул подключений, управляющий ограниченным числом подключений.Ваш
ConnectionPoolclass должен отслеживать количество доступных подключений (начиная с capacity, переданной в constructor). Реализуйте:acquire(): если подключение доступно, уменьшить счётчик и вывестиConnection acquired (X available), где X — оставшееся количество; вернутьtrueпри успешном выполнении иfalse, если доступных подключений нетrelease(): увеличить количество доступных подключений и вывестиConnection released (X available)available(): вернуть текущее количество доступных подключений
ConnectionGuard.h: Создайте оболочку RAII, безопасно управляющую одним подключением.Ваш
ConnectionGuardclass реализует шаблон RAII. Он должен:- Принимать ссылку на
ConnectionPoolв своём constructor и пытаться получить подключение - Хранить информацию о том, успешно ли прошло получение подключения
- Предоставлять метод
isConnected()для проверки того, удерживает ли guard действительное подключение - automatically возвращать подключение в пул в destructor (только если оно было получено)
- Удалить copy constructor и copy assignment, чтобы предотвратить дублирование ресурса (учёт Rule of Five)
Когда выполняется destructor, если подключение удерживалось, выведите
Guard releasing connectionперед вызовом release у пула.- Принимать ссылку на
main.cpp: Продемонстрируйте automatic очистку RAII с помощью областей видимости.Считайте два входных значения:
- Вместимость пула (целое число)
- Количество запрашиваемых подключений (целое число)
Создайте
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;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнерПотренируйтесь самостоятельно: Онлайн-компилятор C++