Deferred statements run at the end of the current scope, regardless of where they’re actually located sequentially in the code. This is useful for cleanup operations, letting you keep them alongside the associated setup code.
The inspiration for deferred code comes from go, but here I’ll show how to implement a similar thing in C++.
Take this extremely simplified hypothetical example from something like a game:
void main(){
setup_graphics();
if(!setup_graphics_success) return;
DEFERRED(cleanup_graphics());
setup_audio();
if(!setup_audio_success) return;
DEFERRED(cleanup_audio());
setup_networking();
if(!setup_networking_success) return;
DEFERRED(cleanup_networking());
while(true){
// run the thing!!!
}
// no need to clean up, deferred blocks will be called here automatically!
}
These deferred statements keep the associated systems “alive” until the end of the scope, which offers two main benefits: code clarity, and automatic state management.
The first benefit is hopefully obvious, keeping the setup and cleanup code together eases the mental tax of replicating setup and cleanup steps in two places. Usually, you’ll want to initialise larger systems, then subsystems, and this can help prevent issues caused by nested setup and cleanup steps falling out of sync as code changes.
The second benefit is more subtle, and is that here we’re using control flow to implicitly handle our system state without tracking it manually. For example, if the setup step for networking fails, we'll only clean up the graphics and audio systems, because those are the only two deferred statements set up by that point. This can prevent having to write any logic for the bad paths.
Note: Similar behaviour could be achieved by using goto on the bad paths to jump to the same clean-up steps placed at the end of the function (in inverse order). However, that can get clunky, and some people prefer to avoid using goto entirely, which makes this approach feel more pragmatic.
If these collocated setup and cleanup sections sound a bit like a constructor / destructor pair, it’s because it kind of is? We can use the fact that destructors are called on stack allocated objects when their containing scope ends to implement this DEFERRED() helper!
In it's simplest form:
#include <functional>
struct Deferred {
std::function<void()> fn;
~Deferred(){ fn(); }
};
This is all we need to make the simplest form of deferred code work, just a callback function that's called in a destructor.
To use it though isn't super convenient. If we do something like this, it doesn't do what we'd want:
#include <print>
int main() {
std::println("Start");
Deferred([](){ std::println("Deferred"); });
std::println("End");
}
The output will be as follows, not running the deferred code at the end of the scope as intended:
Start
Deferred
End
This is because the Deferred object is created as a temporary, so its destructor is called right after it's created, not at the end of the scope. To fix this we have to assign it to a local variable, then the result will be what we want:
#include <print>
int main() {
std::println("Start");
Deferred deferred_thing = {[](){ std::println("Deferred"); }};
std::println("End");
}
Which gives the correct result:
Start
End
Deferred
You could stop here, and create these objects directly, but I usually create a convenience macro that generates a unique name for the local variable and includes the enclosing lambda to save a bunch of syntax noise:
#include <print>
#include <functional>
struct Deferred {
std::function<void()> fn;
~Deferred(){ fn(); }
};
#define CONCAT_INNER(a, b) a ## b
#define CONCAT(a, b) CONCAT_INNER(a, b)
#define DEFERRED(code) Deferred CONCAT(deferred_, __LINE__) = {[&](){code;}};
int main() {
std::println("Start");
DEFERRED(std::println("Deferred"));
std::println("End");
}
This works for a most of situations I need to use deferred code in, but if you're aware of when a reference capturing lambda might not be the right thing to use, you can always tweak the macro!