Идиома 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:
// Переключение состояния, через жизненный цикл класса
explicit 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 был прост, теперь он стал умным.
Косинцев Артём
Инженер-программист