If you’ve ever written Go, you know the joy of defer. It allows you to schedule a function call to be run when the current function returns. It’s perfect for cleanup: closing files, unlocking mutexes, or freeing memory.
Standard C++ uses RAII and destructors to clean up resources. std::unique_ptr, std::lock_guard, and std::vector all do this automatically.
The Old Ways
Before C++11 lambdas, implementing defer was a dark art.
The for Loop Trick
One common trick was abusing the for loop to create a scope:
#define DEFER(code) for (int _i = 0; _i < 1; ++_i, (code))
This sort of works, but it forces you into a nested block syntax. Instead of a simple statement like defer(cleanup()), you’re writing a loop body. This breaks the linear flow of your code and introduces the “arrow code” problem, pushing your logic further to the right.
You have to manually create a scope (using braces {}) for the code you want to execute before the defer triggers:
// Usage
DEFER(fclose(f)) {
// Code that uses f
}
You also run into a major limitation: break and continue. If you use break inside the DEFER block, it breaks out of the defer loop, not your surrounding loop. It’s a leaky abstraction that can lead to subtle bugs.
The std::shared_ptr Abuse
Another “clever” trick was using std::shared_ptr with a custom deleter:
std::shared_ptr<void> defer(nullptr, [&](void*){ fclose(f); });
This works, but it’s heavy. While a smart compiler might optimize away the heap allocation for the control block, you still pay a massive price in compile time. You have to include <memory>, pulling in thousands of lines of standard library code just to run a cleanup function.
The Goal
We want syntax that looks like this:
void process_file(const char* path) {
FILE* f = fopen(path, "r");
if (!f) return;
DEFER(fclose(f)); // Clean up automatically!
// ... do work ...
}
Pre-requisites: The Macros
To implement defer robustly, we need a way to generate unique variable names so we can use DEFER multiple times in the same scope. We’ll use the __COUNTER__ macro for this.
#define NAME_CONCAT_IMPL(x, y) x##y
#define NAME_CONCAT(x, y) NAME_CONCAT_IMPL(x, y)
#define DEFER_UNIQ(prefix) NAME_CONCAT(prefix, __COUNTER__)
Option 1: The Lambda Struct Approach
typedef void (*defer_fn)(void*);
struct DeferGuard {
defer_fn fn;
void* ctx;
DeferGuard(defer_fn f, void* c) : fn(f), ctx(c) {}
~DeferGuard() {
fn(ctx);
}
};
#define DEFER_IMPL(ID, CAP, ...) \
auto NAME_CONCAT(ID, _lam) = CAP() { \
__VA_ARGS__; \
}; \
struct NAME_CONCAT(ID, _Thunk) { \
static void call(void* ctx) { \
(*static_cast<decltype(NAME_CONCAT(ID, _lam))*>(ctx))();\
} \
}; \
DeferGuard NAME_CONCAT(ID, _g)(&NAME_CONCAT(ID, _Thunk)::call, \
&NAME_CONCAT(ID, _lam))
#define DEFER(...) DEFER_IMPL(DEFER_UNIQ(__defer_), [=], __VA_ARGS__)
#define DEFER_REF(...) DEFER_IMPL(DEFER_UNIQ(__defer_), [&], __VA_ARGS__)
Option 2: The Template Approach
template <typename F>
struct Defer {
F f;
Defer(F f) : f(f) {}
~Defer() { f(); }
};
#define DEFER(code) Defer DEFER_UNIQ(_defer_)([=](){ code; })
#define DEFER_REF(code) Defer DEFER_UNIQ(_defer_)([&](){ code; })
This works and is very idiomatic C++. It uses the type system to wrap the lambda, ensuring type safety and allowing the compiler to inline the destructor calls easily.
Usage Examples
Basic Usage (Capture by Value)
By default, DEFER captures by value. This is safer for most cleanup tasks because it preserves the state of variables at the point where DEFER was called.
FILE* f = fopen("data.txt", "rb");
if (f) {
DEFER(fclose(f));
// Read data...
}
Modifying State (Capture by Reference)
If you need the deferred code to see the latest state of a variable, or if you need to modify a variable in the outer scope, use DEFER_REF.
Timer t;
t.start();
DEFER_REF(t.stop());
ImGui Scopes
Instead of worrying about matching every Begin with an End:
if (ImGui::Begin("Settings")) {
DEFER(ImGui::End());
ImGui::Text("Hello");
// ...
}
Temporary Allocators
A common pattern with arena allocators is to create a temporary scope that automatically resets when you’re done.
Temp tmp = get_scratch();
DEFER_REF(temp_end(&tmp));
// Allocate temporary memory
Arena* arena = tmp.arena;
void* data = arena_push(arena, 1024);
// ... use data ...
// Arena automatically resets to saved position when tmp goes out of scope