Суть
Когда-то я долгое время писал на Swift, а в этом языке достаточно много сахара, который упрощает код и прикрывает программисту пятую точку.
Чего только стоит guard, который позволяет проверять условия, не плодя лишние уровни вложенности и не заставляя морочиться с областями видимости.
Но сейчас не о guard.
Был в Swift ещё один приятный механизм - defer.
Да, это примерно та же идея, которая многим знакома по Go: можно отложить выполнение куска кода до момента, когда текущая функция завершится.
Как это выглядит в Go
Например, в Go часто пишут так:
func main() {
file, _ := os.Open("myfile.txt")
defer file.Close()
// Do something with opened file
}То есть мы открываем файл, сразу говорим, что его нужно закрыть при выходе из функции, а дальше спокойно работаем с ним.
Файл закроется независимо от того, как мы выйдем из функции: обычным путём или через ошибку. Насколько я помню, при panic deferred-вызовы тоже выполняются, если выполнение до них дошло.
А что в C++
В C++ ключевого слова defer нет.
Зато есть постоянная потребность освобождать ресурсы:
- закрывать файлы;
- закрывать ключи реестра;
- освобождать дескрипторы;
- откатывать временные изменения;
- возвращать состояние обратно.
И есть принцип RAII: Resource Acquisition Is Initialization.
Если по-человечески: ресурс привязывается к объекту, а когда объект выходит из области видимости, вызывается его деструктор. В деструкторе и выполняется очистка.
После нескольких дней работы с Windows API, где постоянно нужно закрывать то файлы, то ключи реестра, то ещё что-нибудь, я написал маленький кусочек кода, который теперь радостно таскаю из проекта в проект.
defer.hpp
У меня это лежит в файле defer.hpp.
#pragma once
#include <concepts>
#include <functional>
#include <utility>
template <std::invocable Func>
class ScopeGuard {
public:
explicit ScopeGuard(Func func) noexcept
: m_function(std::move(func)) {}
~ScopeGuard() noexcept {
m_function();
}
ScopeGuard(const ScopeGuard&) = delete;
ScopeGuard& operator=(const ScopeGuard&) = delete;
ScopeGuard(ScopeGuard&&) = delete;
ScopeGuard& operator=(ScopeGuard&&) = delete;
private:
Func m_function;
};
#define DEFER_CONCAT_IMPL_(x, y) x##y
#define DEFER_CONCAT_(x, y) DEFER_CONCAT_IMPL_(x, y)
#define defer(code) \
auto DEFER_CONCAT_(defer_object_, __LINE__) = ScopeGuard([&]() noexcept { code; })Использование
Например, открываем ключ реестра через Windows API:
#include <optional>
#include <string>
#include <windows.h>
#include "defer.hpp"
// Some code before
HKEY hKey;
if (RegOpenKeyExW(
HKEY_LOCAL_MACHINE,
regPath.c_str(),
0,
KEY_READ,
&hKey
) != ERROR_SUCCESS) {
return std::nullopt;
}
defer(RegCloseKey(hKey));
// Some code after
Если ключ реестра был открыт успешно, то что бы ни произошло ниже, он будет закрыт при выходе из области видимости.
Можно вернуть результат:
return value;Можно выбросить исключение:
throw std::runtime_error("something went wrong");Всё равно вызовется деструктор объекта ScopeGuard, созданного макросом defer, а он вызовет переданный код:
RegCloseKey(hKey);Почему это работает
Макрос:
defer(RegCloseKey(hKey));разворачивается примерно в такое:
auto defer_object_42 = ScopeGuard([&]() noexcept {
RegCloseKey(hKey);
});Число в имени берётся из __LINE__, чтобы на разных строках можно было писать несколько defer.
Когда выполнение выходит из текущей области видимости, объект defer_object_42 уничтожается. При уничтожении вызывается деструктор, а в деструкторе вызывается лямбда.
Вот и весь фокус.
Несколько defer подряд
Можно написать несколько отложенных действий:
defer(CleanupFirst());
defer(CleanupSecond());Важный момент: порядок выполнения будет соответствовать обычному порядку уничтожения локальных объектов C++ - в обратном порядке их создания.
То есть сначала выполнится CleanupSecond(), потом CleanupFirst().
Это похоже на стек и близко к поведению defer в Go.
Важная оговорка про исключения
Деструктор ScopeGuard объявлен как noexcept.
Это сделано намеренно: бросать исключения из деструктора - плохая идея, особенно если уже идёт раскрутка стека из-за другого исключения.
Поэтому код внутри defer(...) тоже должен быть безопасным и не бросать исключения.
Хорошие кандидаты:
defer(RegCloseKey(hKey));
defer(CloseHandle(handle));
defer(DeleteFileW(tempFilePath.c_str()));Плохой кандидат:
defer(SomeFunctionThatCanThrow());Если функция теоретически может бросить исключение, лучше обработать его внутри:
defer(
try {
SomeFunctionThatCanThrow();
}
catch (...) {
// log if needed
}
);Хотя если таких случаев много, возможно, нужен уже не defer, а нормальная RAII-обёртка.
Когда лучше не использовать такой defer
Этот подход удобен для небольших участков кода, особенно когда работаешь с C API или WinAPI.
Но если ресурс используется часто, лучше написать отдельную RAII-обёртку.
Например, для HKEY можно сделать класс, который сам вызывает RegCloseKey в деструкторе. Для HANDLE - обёртку вокруг CloseHandle.
То есть:
defer(RegCloseKey(hKey));хорошо для быстрого локального решения.
А для постоянного использования в проекте лучше что-то вроде:
class RegistryKey {
public:
~RegistryKey() {
if (m_key) {
RegCloseKey(m_key);
}
}
private:
HKEY m_key = nullptr;
};Чем это отличается от настоящего defer
Понятно, что это не полностью то же самое, что встроенный defer в Swift или Go.
Отличия есть:
- это не ключевое слово языка, а макрос;
- работает через локальный объект и деструктор;
- область действия - текущий C++ scope, а не обязательно вся функция;
- нужно следить, чтобы код очистки не бросал исключения;
- при очень сложном коде макрос может ухудшать читаемость.
Но базовую идею конструкция реализует: написал очистку рядом с захватом ресурса и больше не держишь в голове все возможные return, throw и ветки выхода.
Итог
В C++ нет встроенного defer, но есть RAII. А RAII позволяет довольно просто собрать маленький ScopeGuard.
Для работы с WinAPI и другим C-style API такая штука часто оказывается удобной: открыл ресурс, сразу написал, как его закрыть, и пошёл дальше.
Не серебряная пуля, не замена нормальным RAII-классам, но как маленький инструмент в defer.hpp - вполне рабочая вещь.
Upd сильно позже
Конкретно мою задачу можно было решить использованием WIL, в которой уже всё сделано до меня, причём более безопасно и профессионально. Но тогда я о существовании этой библиотеки не знал, а сделать надо было вот прямо сейчас и на коленке.