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 превращает подверженное ошибкам ручное управление ресурсами в безопасную автоматическую очистку.
Задание
ЛегкоДавайте создадим Менеджер пула соединений, используя 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 через области видимости.Считайте два входных значения:
- Емкость пула (целое число)
- Количество запрашиваемых соединений (целое число)
Создайте
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;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
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Итоги — Обобщенный контейнер