Manual resource management in low level C-style C++ code might be annoying. It's not practical to create good enough RAII wrappers for every single C API you use, but approaches with goto cleanup
or loads of nested if (success)
hurt readability.
A Go-inspired defer
macro to the rescue! The usage is as simple as that:
void* p = malloc(0x1000);
defer [&] { free(p); };
The deferred lambda will be executed on scope exit, no matter how it happens: you can return
from any point, throw
an exception (if allowed), or even use a goto
to an outer scope.
Macro implementation is concise and relies on basic features of 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
It is truly zero-cost and doesn't rely on C runtime or standard library, so it can be used even in kernel development.
Let's compare!
Naive version
Let's imagine a function where all successfully acquired resources are explicitly released on every error:
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;
}
So many duplicated lines of code, so easy to make a mistake and forget to free something!
Classic goto cleanup
The same function, but in the classic goto cleanup
style:
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;
}
You can't goto
through variable declarations so it requires to declare all variables in advance. It's also a bit less effective because the cleanup
part should check if every resource is valid and requires to be released, and you can accidentally forget to free something or do it in wrong order because it's far from the code that acquires the resources so it's harder to notice an error.
Nested if (success)
With the nested if (success)
approach our function would become:
bool MakeDumpToFile(DWORD pid, PCWCHAR filename)
{
bool result = false;
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)
{
result = pfnMiniDumpWriteDump(proc, pid, file, MiniDumpNormal, NULL, NULL, NULL);
CloseHandle(file);
}
CloseHandle(proc);
}
}
FreeLibrary(dbgdll);
}
return result;
}
An improvement, but you'd better have a really wide monitor for this!
WTF std::unique_ptr
The same, but with taste of 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, as usual, delivers the best WTF experience. This hacky approach is provided here for completeness' sake. Some people really use std::unique_ptr
with custom deleters to manage non-pointer resources, even though template argument deduction does not help here, requiring you to specify all those verbose types every time. It has an important limitation: the resource must appear as nullptr
in an invalid state, which is not always the case, and you have to deal with this somehow using additional hacks and tricks.
And finally, defer!
We can rewrite it with our defer
macro this way:
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);
}
This looks much better! No excessive nesting, no so much hated goto
, no duplicated lines of code.
Why this syntax?
Well, what other syntax could it be? Let's think...
defer free(p);
Go-like syntax. Unfortunately, it can't be implemented as a C++ macro.
defer(free(p));
Looks misleading — it seems like free(p)
is called immediately, and its result is passed to defer. Also, it doesn't allow deferring multiple lines of code, which is sometimes useful.
defer { free(p); };
Better, but it doesn't let you control whether outer variables are captured by reference or by copy, which is important in some cases.
defer [&] { free(p); };
Our syntax. It expects a proper lambda, providing flexibility to control whether it captures variables by reference or by copy. In fact, it can defer any callable, not just a lambda — so even the semicolon after the closing brace looks reasonable.
There is also a proposal to add defer
to C, and it uses exactly this syntax.