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 превращает подверженное ошибкам ручное управление ресурсами в безопасную автоматическую очистку.
Задание
ЛегкоДавайте создадим Connection Pool Manager с использованием RAII для безопасного управления соединениями с базой данных. В реальных приложениях соединения с базой данных являются дорогостоящими ресурсами, которые необходимо правильно получать и освобождать. Вы создадите оболочку RAII, гарантирующую, что соединения всегда возвращаются в пул, даже если возникают исключения или пути выполнения кода становятся сложными.
Вы организуете код в трёх файлах:
ConnectionPool.h: Создайте простой пул соединений, который управляет ограниченным числом соединений.Ваш
ConnectionPoolclass должен отслеживать, сколько соединений доступно (начиная с 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, которая безопасно управляет одним соединением.Ваш
ConnectionGuardclass воплощает шаблон RAII. Он должен:- Принимать ссылку на
ConnectionPoolв своём constructor и пытаться получить соединение - Хранить информацию о том, была ли acquisition успешной
- Предоставлять метод
isConnected()для проверки того, хранит ли guard действительное соединение - automatically возвращать соединение в пул в destructor (только если оно было acquired)
- Удалять copy constructor и copy assignment, чтобы предотвратить дублирование ресурса (учёт Rule of Five)
Когда запускается destructor, если соединение удерживалось, выведите
Guard releasing connectionперед вызовом release у пула.- Принимать ссылку на
main.cpp: Продемонстрируйте автоматическую очистку RAII с помощью областей видимости.Считайте два входных значения:
- Ёмкость пула (целое число)
- Число запрашиваемых соединений (целое число)
Создайте
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;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
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++