Pull to refresh

Neat defer macro for C++17

Level of difficultyMedium
Reading time5 min
Views264

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.

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
0
Comments0

Articles