Идиома RAII — давно зарекомендовал себя как удобный способ автоматического управления ресурсами в C++. Обычно мы применяем его для управления памятью, файловыми дескрипторами или мьютексами. Однако что, если расширить понятие RAII до управления не только физическими ресурсами, но и логическими контрактами и состояниями системы?
В этой статье я хочу поговорить о том, как RAII можно использовать для контроля жизненного цикла асинхронных операций, транзакций или подписок, гарантируя их корректное завершение или откат до прежнего состояния.

Классическое определение RAII — это автоматическое управление ресурсом через объект, чья ответственность завершается в деструкторе. Это удобно и безопасно. Но если абстрагироваться от «ресурса» и подумать шире — мы получаем мощный механизм, который может управлять чем угодно, у кого есть чётко определённый жизненный цикл.
Примеры в статье сделаны намеренно упрощёнными и потенциально могут содержать уязвимые места, требующие доработки в реальном коде.
Пример 1: Управление подписками
Предположим, у нас есть система эвентов, где подписка оформляется через объект Subscription. В классическом варианте реализация выглядит следующим образом:
class Subscription { public: using UnsubscribeFunc = std::function< void() >; // Базовый конструктор, принимающий функцию отписки Subscription( UnsubscribeFunc func ) : mUnsubscribeFunc( std::move( func ) ) { } // В деструкторе происходит выполнение логики удаления из очереди подписчиков ~Subscription() { if( mUnsubscribeFunc ) mUnsubscribeFunc(); } // Стандартные методы копирования лучше запретить Subscription( const Subscription& ) = delete; Subscription& operator=( const Subscription& ) = delete; // Оставляем только перемещение, это безопасно Subscription( Subscription&& other ) noexcept : mUnsubscribeFunc( std::move( other.mUnsubscribeFunc ) ) { // Если не затереть у перемещаемого объекта // деструктор other выполнит отписку практически в тот же момент other.mUnsubscribeFunc = nullptr; } Subscription& operator=( Subscription&& other ) noexcept { if( this != &other ) { mUnsubscribeFunc = std::move( other.mUnsubscribeFunc ); other.mUnsubscribeFunc = nullptr; } return *this; } private: UnsubscribeFunc mUnsubscribeFunc; };
Далее для наглядности, в рамках паттерна «Наблюдатель» реализуем Observable, в котором подписка оформлена через RAII:
class Observable { public: using Callback = std::function< void( int ) >; // Подписка: возвращает RAII-объект Subscription с уникальным идентификатором Subscription Subscribe( Callback cb ) { std::lock_guard< std::mutex > lock( mMutex ); // Исключительно для примера сделаем уникальность подписки через счетчик uint64_t id = ++mIdCounter; mSubscribers.emplace_back( SubscriptionEntry{ id, std::move( cb ) } ); return Subscription( [ this, id ]() { // Здесь то мы и реализуем метод отписки для деструктора Subscription std::lock_guard< std::mutex > lock( mMutex ); auto it = std::remove_if( mSubscribers.begin(), mSubscribers.end(), [ id ]( const SubscriptionEntry& entry ) { return entry.mId == id; }); if( it != mSubscribers.end() ) { mSubscribers.erase( it, mSubscribers.end() ); std::cout << "[Subscription] Отписка выполнена для id: " << id << "\n"; } } ); } // Эмитирование события void Emit( int value ) { std::lock_guard< std::mutex > lock( mMutex ); for( const auto& entry : mSubscribers ) { entry.mCallback( value ); } } private: struct SubscriptionEntry { uint64_t mId; Callback mCallback; }; std::vector< SubscriptionEntry > mSubscribers; std::mutex mMutex; std::atomic< uint64_t > mIdCounter{ 0 }; };
Пример использования:
{ // Выполнение лямбды плотно завязано на длительность жизни sub auto sub = obs.subscribe([](int val) { std::cout << "value: " << val << "\n";}); obs.emit(10); obs.emit(100); } // Здесь sub уничтожен — выполнена отписка obs.emit(500); // Данный эмит не будет пойман в рамках подписки sub
Благодаря RAII, управление подпиской становится безопасным и предсказуемым. Мы больше не нуждаемся в ручных вызовах Unsubscribe(), поскольку область видимости того, кто владеет подпиской, определяет её жизненный цикл.
Пример 2: Логические контракты и транзакции
Рассмотрим, как RAII может быть приспособлен для логических контрактов: управление транзакцией и логика отката. Предположим, что у нас есть логическая операция — транзакция, которую необходимо либо зафиксировать, либо откатить в случае ошибки. Пример реализации транзакции с методами Commit и Rollback:
struct Transaction { int mId; void Commit() { std::cout << "Transaction " << mId << " committed.\n"; } void Rollback() { std::cout << "Transaction " << mId << " rolled back.\n"; } };
А также guard, который будет косвенно управлять состоянием транзакции:
template< typename T > class OwnershipGuard { public: OwnershipGuard( T* ptr, std::function< void( T* ) > rollbackAction ) : mPtr( ptr ) , mRollbackAction( std::move( rollbackAction ) ) , mRollbackEnabled( true ) { } ~OwnershipGuard() { if( mRollbackEnabled && mPtr ) { // Если откат всё ещё включён, вызываем rollback mRollbackAction( mPtr ); } } // Аналогично предыдущему примеру - запрещено для устранения дублей OwnershipGuard( const OwnershipGuard& ) = delete; OwnershipGuard& operator=( const OwnershipGuard& ) = delete; OwnershipGuard( OwnershipGuard&& other ) noexcept : mPtr( other.mPtr ) , mRollbackAction( std::move( other.mRollbackAction ) ) , mRollbackEnabled( other.mRollbackEnabled ) { other.mRollbackEnabled = false; } OwnershipGuard& operator=( OwnershipGuard&& other ) noexcept { if( this != &other ) { if( mRollbackEnabled && mPtr ) { mRollbackAction( mPtr ); } mPtr = other.mPtr; mRollbackAction = std::move( other.mRollbackAction ); mRollbackEnabled = other.mRollbackEnabled; other.mRollbackEnabled = false; } return *this; } // Будет вызвано лишь в случае успешного исполнения логики по транзакции void Release() { mRollbackEnabled = false; } T* Get() const { return mPtr; } private: T* mPtr; std::function< void( T* ) > mRollbackAction; bool mRollbackEnabled; };
Использование:
void ProcessTransaction() { // Создаем абстрактную транзакцию со своей логикой Transaction txn{ 42 }; // Устанавливаем лямбду роллбека на случай возможных неприятностей OwnershipGuard< Transaction > guard( &txn, []( Transaction* t ) { t->Rollback(); } ); // Сделаем заведомо ложным (можно выбросить здесь исключение) bool success = false; if( success ) { guard.Release(); txn.Commit(); } // Иначе rollback при выходе из области видимости }
Таким образом, этот guard служит контрактом: если транзакция не завершена явно, она автоматически откатывается. RAII берёт на себя ответственность за корректное логическое завершение транзакции, избавляя от необходимости повторных проверок.
Пример 3: Управление асинхронными операциями в многопоточном окружении
RAII особенно важен в многопоточной среде, поскольку гарантирует корректное освобождение ресурсов даже в случае возникновения исключений. Пример ниже показывает применение RAII для управления флагом отмены асинхронных задач:
class CancelGuard { public: // Переключение состояния, через жизненный цикл класса exp licit CancelGuard( std::atomic< bool >& cancelFlag ) : mCancelFlag( cancelFlag ) { mCancelFlag.store( false, std::memory_order_release ); } ~CancelGuard() { mCancelFlag.store( true, std::memory_order_release ); } CancelGuard( const CancelGuard& ) = delete; CancelGuard& operator=( const CancelGuard& ) = delete; private: std::atomic< bool >& mCancelFlag; };
// Пример плохой, но асинхронной задачи: void AsyncOperation( std::atomic< bool >& cancelFlag ) { using namespace std::chrono_literals; while( !cancelFlag.load( std::memory_order_acquire ) ) { std::cout << "Working...\n"; std::this_thread::sleep_for( 100ms ); } std::cout << "Operation cancelled.\n"; } int main() { std::atomic< bool > cancel_flag{ false }; std::future< void > future; { // RAII: при выходе из блока произойдёт отмена всех асинхронных задач CancelGuard guard( cancel_flag ); // Асинхронный запуск future = std::async( std::launch::async, AsyncOperation, std::ref( cancel_flag )); // Имитируем работу std::this_thread::sleep_for( std::chrono::seconds( 1 ) ); // За данным блоком future-задачи теряют актуальность - будут отменены } // Данный wait() завершится мгновенно future.wait(); }
Как видно из примера, объект CancelGuard управляет жизненным циклом отмены: пока он существует, асинхронная операция считается актуальной. При уничтожении объекта происходит автоматическая отмена, что гарантирует согласованность работы системы.
⚠️ Важно: безопасность кода в деструкторах
Применяя RAII для управления логикой (отписки, откатов, завершения задач), важно помнить, что деструкторы не должны выбрасывать исключения. Выброс в деструкторе при раскрутке стека из-за другого исключения может привести к std::terminate.
По возможности:
-
Делегируйте опасные действия (сеть, ввод-вывод) из деструктора в асинхронный или отложенный механизм (например,
std::jthread,std::asyncи т.д.). -
Используйте деструктор только для гарантированного и безопасного изменения локального состояния.
-
При необходимости — подавляйте исключения внутри деструктора и логируйте их.
RAII не отменяет здравого смысла — он усиливает архитектуру, но требует ответственности в реализации.
Заключение
Подход универсален. RAII может управлять чем угодно, где важна чёткая финализация: права доступа, сессии, проверка инвариантов, очистка реактивных состояний. Надеюсь, в рамках статьи мне удалось показать, что RAII не про файлы, которые удобно закрывать в деструкторе. Область применения данной идиомы выходит далеко за рамки общеизвестной реализации мьютексов стандартной библиотеки, позволяя создавать более надёжные и отказоустойчивые приложения.
Если вы проектируете систему, в которой присутствуют:
-
подписки,
-
транзакции,
-
асинхронные ожидания,
-
жизненные циклы логических фаз и т.д. и т.д.
подумайте: можно ли управлять этим через RAII? Если да — вы получите не просто лаконичный код, а архитектуру, устойчивую к ошибкам, исключениям и забывчивости.
RAII был прост, теперь он стал умным.
Косинцев Артём
Инженер-программист
ссылка на оригинал статьи https://habr.com/ru/articles/901092/
Добавить комментарий