Ручное управление ресурсами в низкоуровневом си-подобном коде на 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, и оно использует именно такой синтаксис.
C++26
Реализация макроса еще проще с грядущим C++26, где можно создавать безымянные переменные (Clang 18+, GCC 14+):
#ifndef defer template <typename T> struct Deferrer { T f; Deferrer(T f) : f(f) { }; Deferrer(const Deferrer&) = delete; ~Deferrer() { f(); } }; #define defer Deferrer _ = #endif
Комментарии временно закрыты.