Ежегодное управление ресурсами в низкоуровневом коде 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);
}
вернуть результат;
}
Нельзя перейти к разделу, состоящему из объявлений переменных, поэтому все переменные необходимо объявлять заранее. Это также менее эффективно, поскольку часть кода, отвечающая за очистку ресурсов, должна проверять, действительны ли все ресурсы и требуют ли они освобождения, а вы можете случайно забыть освободить что-то или сделать это в неправильном порядке, поскольку это происходит далеко от кода, который получает ресурсы, поэтому заметить ошибку сложнее.
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);
}
вернуть результат;
}
Улучшение есть, но для этого вам понадобится действительно широкоформатный монитор!
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 в недопустимом состоянии, что происходит не всегда, и с этим приходится как-то справляться, используя дополнительные хаки и уловки.
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); };
Наш синтаксис. Он ожидает корректную лямбда-функцию, обеспечивая гибкость в управлении тем, захватывает ли она переменные по ссылке или по копированию. Фактически, он может отложить вызов любой вызываемой функции, а не только лямбда-функции — поэтому даже точка с запятой после закрывающей фигурной скобки выглядит вполне уместно.
На помощь приходит макрос `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); };
Наш синтаксис. Он ожидает корректную лямбда-функцию, обеспечивая гибкость в управлении тем, захватывает ли она переменные по ссылке или по копированию. Фактически, он может отложить вызов любой вызываемой функции, а не только лямбда-функции — поэтому даже точка с запятой после закрывающей фигурной скобки выглядит вполне уместно.