Удобный макрос `defer` для C++17

WILD

Administrator
Staff member
ADMIN
SELLER
SUPREME
MEMBER
Joined
Jan 21, 2025
Messages
219
Reaction score
637
Deposit
0$
Ежегодное управление ресурсами в низкоуровневом коде C++ в стиле C может быть утомительным. Создание достаточно хороших RAII-оберток для каждого используемого API на C нецелесообразно, но подходы с использованием goto для очистки или множеством вложенных операторов if (success) ухудшают читаемость.

На помощь приходит макрос `defer`, вдохновленный языком Go! Использовать его очень просто:

void* p = malloc(0x1000);
defer [&] { free(p); };


Отложенная лямбда-функция будет выполнена при выходе из области видимости, независимо от способа выхода: вы можете вернуться из любой точки, сгенерировать исключение (если это разрешено) или даже использовать переход во внешнюю область видимости.

Реализация макросов лаконична и основана на базовых возможностях C++17 (Clang 5+, GCC 7+, MSVC 2017+):

#ifndef defer

шаблон <typename T>
структура отложенного выполнения
{
Т ф;
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


Это действительно бесплатное решение , не зависящее от среды выполнения C или стандартной библиотеки, поэтому его можно использовать даже при разработке ядра.

Давайте сравним!​

Наивная версия​

Представим себе функцию, в которой все успешно полученные ресурсы явно освобождаются при каждой ошибке:

bool MakeDumpToFile(DWORD pid, PCWCHAR filename)
{
HMODULE dbgdll = LoadLibraryA("dbghelp.dll");
если (!dbgdll)
{
вернуть false;
}

auto pfnMiniDumpWriteDump = (decltype(&MiniDumpWriteDump)) GetProcAddress(dbgdll, "MiniDumpWriteDump");
if (!pfnMiniDumpWriteDump)
{
FreeLibrary(dbgdll);
вернуть false;
}

HANDLE proc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
если (!proc)
{
FreeLibrary(dbgdll);
вернуть 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);
вернуть false;
}

bool result = pfnMiniDumpWriteDump(proc, pid, file, MiniDumpNormal, NULL, NULL, NULL);

CloseHandle(file);
CloseHandle(proc);
FreeLibrary(dbgdll);

вернуть результат;
}


Столько повторяющихся строк кода, так легко допустить ошибку и забыть освободить память!

Классическая очистка по умолчанию​

Та же функция, но в классическом стиле очистки с помощью команды «Перейти»:

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);

очистка:

if (file && file != INVALID_HANDLE_VALUE)
{
CloseHandle(file);
}

если (процедура)
{
CloseHandle(proc);
}

если (dbgdll)
{
FreeLibrary(dbgdll);
}

вернуть результат;
}


Нельзя перейти к разделу, состоящему из объявлений переменных, поэтому все переменные необходимо объявлять заранее. Это также менее эффективно, поскольку часть кода, отвечающая за очистку ресурсов, должна проверять, действительны ли все ресурсы и требуют ли они освобождения, а вы можете случайно забыть освободить что-то или сделать это в неправильном порядке, поскольку это происходит далеко от кода, который получает ресурсы, поэтому заметить ошибку сложнее.

Вложенный if (успех)​

При использовании подхода с вложенными условиями (если выполняется условие) наша функция будет выглядеть следующим образом:

bool MakeDumpToFile(DWORD pid, PCWCHAR filename)
{
bool result = false;
HMODULE dbgdll = LoadLibraryA("dbghelp.dll");

если (dbgdll)
{
auto pfnMiniDumpWriteDump = (decltype(&MiniDumpWriteDump)) GetProcAddress(dbgdll, "MiniDumpWriteDump");
if (pfnMiniDumpWriteDump)
{
HANDLE proc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
если (процедура)
{
HANDLE file = CreateFileW(filename, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (file && file != INVALID_HANDLE_VALUE)
{
result = pfnMiniDumpWriteDump(proc, pid, file, MiniDumpNormal, NULL, NULL, NULL);
CloseHandle(file);
}
CloseHandle(proc);
}
}
FreeLibrary(dbgdll);
}

вернуть результат;
}


Улучшение есть, но для этого вам понадобится действительно широкоформатный монитор!

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` следующим образом:

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 { free(p); };


Лучше, но это не позволяет контролировать, будут ли внешние переменные захватываться по ссылке или путем копирования, что важно в некоторых случаях.

defer [&] { free(p); };


Наш синтаксис. Он ожидает корректную лямбда-функцию, обеспечивая гибкость в управлении тем, захватывает ли она переменные по ссылке или по копированию. Фактически, он может отложить вызов любой вызываемой функции, а не только лямбда-функции — поэтому даже точка с запятой после закрывающей фигурной скобки выглядит вполне уместно.
 
Top Bottom