В функции на C++23 нужно выполнить отмену побочного эффекта при любом выходе до точки подтверждения; какой стандартный механизм RAII выбрать?
Используйте std::scope_exit из заголовка <scope>. Он регистрирует действие, которое выполняется при разрушении guard-объекта, поэтому срабатывает как при обычном выходе из области видимости, так и при выходе через исключение. После успешного подтверждения действие можно отменить вызовом release().
До C++23 стандартная библиотека не содержала универсального scope guard. Для гарантированного освобождения ресурсов обычно применяли специальные RAII-классы, std::unique_ptr с пользовательским удалителем или самописные guards.
Проблема особенно заметна для действий, которые не являются владением ресурса в обычном смысле: отката изменения, удаления временного файла, восстановления флага или отмены регистрации callback. std::scope_exit стандартизирует именно такую логику и делает её локальной для нужной области видимости.
Если очистка записана вручную в конце функции, она может не выполниться при раннем return или исключении. В результате состояние объекта, файла, транзакции или внешнего ресурса останется частично изменённым.
Неверно также безусловно выполнять откат: после успешного завершения операции он может уничтожить уже подтверждённый результат. Поэтому guard должен быть активен до точки успеха, а затем явно отключаться.
std::scope_exit хранит переданную вызываемую сущность и при разрушении проверяет, не был ли guard отключён. Если guard активен, вызываемая сущность выполняется. Разрушение происходит автоматически при любом выходе из области видимости, включая исключение.
До вызова release() функция откатывает value при любом выходе. После release() guard становится неактивным, поэтому успешный результат сохраняется.
Обычно callback захватывает данные по ссылке, но эти данные должны жить дольше guard-а. Порядок разрушения локальных объектов обратен порядку их создания, поэтому guard следует создавать после объектов, которыми он пользуется.
std::scope_exit не делает операцию транзакционной и не обеспечивает потокобезопасность. Он также не отменяет уже выполненные внешние эффекты автоматически: callback должен содержать реальную компенсационную логику. Исключение из callback разрушителя недопустимо для безопасного управления потоком и может привести к std::terminate, поэтому cleanup-функция должна быть фактически не бросающей.
Если действие нужно выполнять только при исключении, применяют std::scope_fail, а только при нормальном выходе — std::scope_success. Для универсального действия независимо от причины выхода подходит именно std::scope_exit.
Сервис изменяет несколько полей объекта, а затем отправляет подтверждение во внешнюю систему. Если отправка не удалась, локальные изменения требуется отменить. Ручной вызов отката в каждой ветви хрупок: новая ветвь с return легко обойдёт очистку.
Самописный RAII-класс даёт полный контроль, но увеличивает объём кода и требует отдельно проверять перемещение, отключение и исключения. std::unique_ptr с пользовательским удалителем тоже может решить задачу, однако его семантика владения плохо отражает смысл «выполнить действие при выходе».
Выбранный вариант — std::scope_exit с callback отката и последующим release() после успешного подтверждения. Это уменьшает число путей, которые нужно вручную проверять, и делает гарантию отката видимой рядом с началом опасной операции. При этом сам внешний вызов и callback должны иметь чётко определённую компенсационную семантику.
Вопрос: Что произойдёт, если вызвать release() до выхода из области видимости?
Ответ: Guard станет неактивным, и его callback больше не будет вызван при разрушении. release() не выполняет действие немедленно и не освобождает захваченные объекты; он только отменяет автоматический вызов. Поэтому его следует вызывать после полного успешного завершения операции.
Вопрос: Безопасно ли захватывать локальную переменную по ссылке в callback std::scope_exit?
Ответ: Да, если guard разрушается раньше этой переменной. Локальные объекты разрушаются в обратном порядке создания, поэтому переменная, созданная до guard-а, ещё существует во время выполнения callback. Нельзя возвращать guard наружу или перемещать его в более долгоживший объект, если callback продолжит ссылаться на уже уничтоженные локальные данные.
Вопрос: Гарантирует ли std::scope_exit откат при завершении программы через std::terminate или std::abort?
Ответ: Нет. Guard работает только при обычном разрушении объекта: нормальном выходе из области видимости или распространении исключения. При std::abort, аварийном завершении процесса или вызове std::terminate стек обычно не разматывается, поэтому деструкторы локальных объектов и callback std::scope_exit не выполняются.