Ручное управление ресурсами в низкоуровневом си-подобном коде на C++ — довольно хлопотное занятие. Создание достойных RAII-врапперов для каждого используемого сишного API не всегда практично, а использование подходов с goto cleanup
или множеством вложенных if (success)
вредит читаемости кода.
Макрос defer
, вдохновленный Go, как никогда кстати! Использовать его просто:
void* p = malloc(0x1000);
defer [&] { free(p); };
Отложенная лямбда будет выполнена при выходе из области видимости, независимо от того, будет ли выполнен return
, брошено исключение (если разрешено), или даже выполнен goto
наружу.
Реализация макроса лаконична и полагается на C++17 (Clang 5+, GCC 7+, MSVC 2017+):
#ifndef defer
template <typename T>
struct deferrer
{
T f;
deferrer(T f) : f(f) { };
deferrer(const deferrer&) = delete;
~deferrer() { f(); }
};
#define TOKEN_CONCAT_NX(a, b) a ## b
#define TOKEN_CONCAT(a, b) TOKEN_CONCAT_NX(a, b)
#define defer deferrer TOKEN_CONCAT(__deferred, __COUNTER__) =
#endif
Данный макрос по-настоящему zero-cost и не зависит от рантайма C или стандартной библиотеки, поэтому его можно использовать даже в разработке под ядро ОС.
Давайте сравним
Наивная версия
Представим функцию, где все выделенные ресурсы освобождаются явно при каждой ошибке:
bool MakeDumpToFile(DWORD pid, PCWCHAR filename)
{
HMODULE dbgdll = LoadLibraryA("dbghelp.dll");
if (!dbgdll)
{
return false;
}
auto pfnMiniDumpWriteDump = (decltype(&MiniDumpWriteDump)) GetProcAddress(dbgdll, "MiniDumpWriteDump");
if (!pfnMiniDumpWriteDump)
{
FreeLibrary(dbgdll);
return false;
}
HANDLE proc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (!proc)
{
FreeLibrary(dbgdll);
return false;
}
HANDLE file = CreateFileW(filename, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (!file || file == INVALID_HANDLE_VALUE)
{
CloseHandle(proc);
FreeLibrary(dbgdll);
return false;
}
bool result = pfnMiniDumpWriteDump(proc, pid, file, MiniDumpNormal, NULL, NULL, NULL);
CloseHandle(file);
CloseHandle(proc);
FreeLibrary(dbgdll);
return result;
}
Выглядит плохо. Много продублированных строк кода, можно легко ошибиться и забыть освободить что-то.
Классический goto cleanup
Та же функция, но в классическом стиле goto cleanup
:
bool MakeDumpToFile(DWORD pid, PCWCHAR filename)
{
bool result = false;
HMODULE dbgdll = NULL;
decltype(&MiniDumpWriteDump) pfnMiniDumpWriteDump = nullptr;
HANDLE proc = NULL;
HANDLE file = NULL;
dbgdll = LoadLibraryA("dbghelp.dll");
if (!dbgdll) { goto cleanup; }
pfnMiniDumpWriteDump = (decltype(&MiniDumpWriteDump)) GetProcAddress(dbgdll, "MiniDumpWriteDump");
if (!pfnMiniDumpWriteDump) { goto cleanup; }
proc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (!proc) { goto cleanup; }
file = CreateFileW(filename, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (!file || file == INVALID_HANDLE_VALUE) { goto cleanup; }
result = pfnMiniDumpWriteDump(proc, pid, file, MiniDumpNormal, NULL, NULL, NULL);
cleanup:
if (file && file != INVALID_HANDLE_VALUE)
{
CloseHandle(file);
}
if (proc)
{
CloseHandle(proc);
}
if (dbgdll)
{
FreeLibrary(dbgdll);
}
return result;
}
Нельзя перепрыгнуть через объявления переменных, поэтому приходится объявить их заранее. Этот код также немного менее эффективен, так как в cleanup
повторно проверяется валидность значений, чтобы определить, какие из ресурсов нужно освобождать. Код освобождения находится далеко от кода выделения, так что легко не заметить ошибку, если вы забыли что-то освободить или сделали это в неправильном порядке.
Вложенные if (success)
При подходе с вложенными if (success)
наша функция будет выглядеть так:
bool MakeDumpToFile(DWORD pid, PCWCHAR filename)
{
HMODULE dbgdll = LoadLibraryA("dbghelp.dll");
if (dbgdll)
{
auto pfnMiniDumpWriteDump = (decltype(&MiniDumpWriteDump)) GetProcAddress(dbgdll, "MiniDumpWriteDump");
if (pfnMiniDumpWriteDump)
{
HANDLE proc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (proc)
{
HANDLE file = CreateFileW(filename, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (file && file != INVALID_HANDLE_VALUE)
{
bool result = pfnMiniDumpWriteDump(proc, pid, file, MiniDumpNormal, NULL, NULL, NULL);
CloseHandle(file);
return result;
}
CloseHandle(proc);
}
}
FreeLibrary(dbgdll);
}
return false;
}
Уже лучше, но из-за излишней вложенности вам лучше иметь монитор пошире.
WTF std::unique_ptr
То же самое, но со вкусом STL:
bool MakeDumpToFile(DWORD pid, PCWCHAR filename)
{
std::unique_ptr<std::remove_pointer_t<HMODULE>, decltype(&FreeLibrary)> dbgdll(LoadLibraryA("dbghelp.dll"), &FreeLibrary);
if (!dbgdll) { return false; }
auto pfnMiniDumpWriteDump = (decltype(&MiniDumpWriteDump)) GetProcAddress(dbgdll.get(), "MiniDumpWriteDump");
if (!pfnMiniDumpWriteDump) { return false; }
std::unique_ptr<std::remove_pointer_t<HANDLE>, decltype(&CloseHandle)> proc(OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid), &CloseHandle);
if (!proc) { return false; }
std::unique_ptr<std::remove_pointer_t<HANDLE>, decltype(&CloseHandle)> file([&]{
auto h = CreateFileW(filename, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
return (h != INVALID_HANDLE_VALUE) ? h : NULL;
}(), &CloseHandle);
if (!file) { return false; }
return pfnMiniDumpWriteDump(proc.get(), pid, file.get(), MiniDumpNormal, NULL, NULL, NULL);
}
STL, как всегда, даёт наилучший WTF-опыт. Некоторые люди действительно используют этот трюк с std::unique_ptr
для автоматического освобождения не указателей, хотя автоматический вывод аргументов шаблона здесь не работает и требуется каждый раз указывать все эти многословные типы. Есть и важное ограничение: ресурс должен быть nullptr
в невалидном состоянии, что не всегда так, из-за чего приходится использовать дополнительные хаки и трюки.
И, наконец, defer!
Мы можем переписать эту функцию с использованием макроса defer
таким образом:
bool MakeDumpToFile(DWORD pid, PCWCHAR filename)
{
HMODULE dbgdll = LoadLibraryA("dbghelp.dll");
if (!dbgdll) { return false; }
defer [&] { FreeLibrary(dbgdll); };
auto pfnMiniDumpWriteDump = (decltype(&MiniDumpWriteDump)) GetProcAddress(dbgdll, "MiniDumpWriteDump");
if (!pfnMiniDumpWriteDump) { return false; }
HANDLE proc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (!proc) { return false; }
defer [&] { CloseHandle(proc); };
HANDLE file = CreateFileW(filename, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (!file || file == INVALID_HANDLE_VALUE) { return false; }
defer [&] { CloseHandle(file); };
return pfnMiniDumpWriteDump(proc, pid, file, MiniDumpNormal, NULL, NULL, NULL);
}
Это выглядит гораздо лучше! Нет излишней вложенности, нет ненавистного goto
, нет дублирования строк кода.
Почему именно такой синтаксис?
А как иначе оно могло бы выглядеть? Давайте посмотрим.
defer free(p);
Вариант в стиле Go. К сожалению, это невозможно реализовать в виде макроса для C++.
defer(free(p));
Это выглядит так, будто free(p)
вызывается немедленно, и его результат передается в defer
. Также это не позволяет помещать в defer
несколько строк кода, что иногда полезно.
defer { free(p); };
Уже лучше, но мы не можем контролировать, хотим ли мы захватывать внешние переменные по значению или по ссылке, что важно в некоторых случаях.
defer [&] { free(p); };
Наш синтаксис. Он ожидает полноценную лямбду, обеспечивая необходимую гибкость, позволяя захватывать внешние переменные по ссылке или по значению. На самом деле, этот макрос может принять любой callable, а не только лямбды, так что даже точка с запятой после закрывающей фигурной скобки выглядит уместной.
Также существует предложение добавить defer
в C, и оно использует именно такой синтаксис.
Комментарии (15)
JordanCpp
09.06.2025 07:30Современные фичи С++ это хорошо, правильно. Но если уж исходить из юзабилити, вполне понятный вариант второй. Проверил, прочитал, закрыл. Никто же не мешает отделить LoadLibraryA и FreeLibrary в класс и дергать его в данном методе при выходе из функции сработает деструктор. Написать шаблон с HANDLE, который в деструкторе будет всегда CloseHandle(file) это делать.
Помню, вроде на rsdn подобный код был, типа заворачиваем WinApi в ООП с конструкторами и деструкторами.
bool MakeDumpToFile(DWORD pid, PCWCHAR filename) { HMODULE dbgdll = LoadLibraryA("dbghelp.dll"); if (dbgdll) { auto pfnMiniDumpWriteDump = (decltype(&MiniDumpWriteDump)) GetProcAddress(dbgdll, "MiniDumpWriteDump"); if (pfnMiniDumpWriteDump) { HANDLE proc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); if (proc) { HANDLE file = CreateFileW(filename, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (file && file != INVALID_HANDLE_VALUE) { bool result = pfnMiniDumpWriteDump(proc, pid, file, MiniDumpNormal, NULL, NULL, NULL); CloseHandle(file); return result; } CloseHandle(proc); } } FreeLibrary(dbgdll); } return false; }
JordanCpp
09.06.2025 07:30Придумал, ещё вариант по старинке:) Класс Library и Handler дергают деструктор
FreeLibrary и CloseHandle.
bool MakeDumpToFile(DWORD pid, PCWCHAR filename) { Library dbgdll("dbghelp.dll"); if (dbgdll.Ok()) { auto pfnMiniDumpWriteDump = (decltype(&MiniDumpWriteDump)) dbgdll.GetAddress("MiniDumpWriteDump"); if (pfnMiniDumpWriteDump) { Handler<HANDLE> proc(procOpenProcess(PROCESS_ALL_ACCESS, FALSE, pid)); if (proc.Ok()) { Handler<HANDLE> file(CreateFileW(filename, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL)); if (file.Ok() && file.Get() != INVALID_HANDLE_VALUE) { bool result = pfnMiniDumpWriteDump(proc.Get(), pid, file.Get(), MiniDumpNormal, NULL, NULL, NULL); return result; } } } } return false; }
icerasp
09.06.2025 07:30Выглядит потрясающе. Честно говоря, еще со школьных времен, когда только-только начинал изучать программирование, никогда не понимал, почему в любой более-менее сложной программе на C/C++ освобождение ресурсов превращается в танцы с бубном. Неужели на столько лет существование языков никому из разработчиков стандартов в голову не приходило как-то упростить этот постоянно использующийся процесс?
JordanCpp
09.06.2025 07:30Ещё вариант, без мам, пап и unique_ptr
bool MakeDumpToFile(DWORD pid, PCWCHAR filename) { Library dbgdll("dbghelp.dll"); if (!dbgdll.Ok()) return false; auto pfnMiniDumpWriteDump = (decltype(&MiniDumpWriteDump)) dbgdll.GetAddress("MiniDumpWriteDump"); if (!pfnMiniDumpWriteDump) return false; Handler<HANDLE> proc(procOpenProcess(PROCESS_ALL_ACCESS, FALSE, pid)); if (!proc.Ok()) return false; Handler<HANDLE> file(CreateFileW(filename, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL)); if (!file.Ok() && file.Get() == INVALID_HANDLE_VALUE) return false; bool result = pfnMiniDumpWriteDump(proc.Get(), pid, file.Get(), MiniDumpNormal, NULL, NULL, NULL); return result; }
domix32
09.06.2025 07:30А откуда этот Handler взялся?
JordanCpp
09.06.2025 07:30Примерно так, так как HANDLE используется чуть менее чем во всем WinAPI облегчит жизнь. Это общая идея. Таким образом типизировать другие типы. Добавить inline, но со сборкой O2 думаю и так компилятор догадается.
template <typename T> class Handler { public: Handler(T handle) : _handle(handle) { } ~Handler() { if (_handle != nullptr) CloseHandle(_handle); } T Get() { return _handle; } bool Ok() { return _handle != nullptr; } private: T _handle; };
Kelbon
09.06.2025 07:30Предлагаю не изобретать того что написано невероятное количество раз и использовать готовое и продуманное
https://github.com/kelbon/logic_guards
domix32
09.06.2025 07:30std::unique_ptr<std::remove_pointer_t<HANDLE>,decltype(&CloseHandle)>
Именно для этого и придуманы алиасы и авто. Ну и как предложили выше можно сделать нормальную обёртку конкретно для виндовых структур.
JordanCpp
09.06.2025 07:30Ну и как предложили выше можно сделать нормальную обёртку конкретно для виндовых структур.
Это будет оптимальнее. Хотя желание автора тоже понятно. Некий стандартный универсальный механизм.
JordanCpp
09.06.2025 07:30Мне не нравится вариант с unique_ptr тем, что будет по крайней мере один new. Банальный пример, а уже лезем в память. Возможно, что как то все оптимизируется. Я не вникал, как оно там устроено. Если, что поправьте.
Kelbon
Таких случаев нет
Также ваш тип копирует лямбду которую захватывает(а не мувает), что дополнительно создает проблемы