I written about defer
before.
This is a keyword that delays execution of a statement until the end of the block.
It’s designed to allow you to perform setup and immediately specify the associated clean up.
I thought it made control flow a bit more confusing but
could be a useful tool.
Simple implementation
C++ doesn’t have this keyword but we can implement something similar.
class Deferred { public: using Function = std::function<void()>; Deferred(const Function& function) : m_Function(function) { } ~Deferred() { m_Function(); } private: Function m_Function; };
Which can be used like this:
FILE* file = fopen(filename, "r"); Deferred deferred([&file]() { fclose(file); });
You can add a bit more flexibility with another constructor:
... template <typename Function, typename Value> Deferred(const Function& function, Value& value) : Deferred([&function, &value]() { function(value); }) { } ...
That gives you the option to pass a function and value rather then forcing you to use a closure:
FILE* file = fopen(filename, "r"); Deferred deferred(fclose, file);
You can even get it to work with function that takes an arbitrary number of values. I won’t go down a template rabbit hole here though. In either case the file will be closed at the end of the scope.
Nested blocks
Today I came across a more complicated situation. Effectively it was something like this:
FILE* file = nullptr; if (useFile) { file = fopen(filename, "r"); } // Do other things. ... if (useFile) { fclose(file); }
It’s not complicated.
We might have opened a file but we still want to be sure that it’s closed.
We can’t use Deferred
in the same way.
If we declare deferred
inside the if-statement then it will close the file too early.
If we declare deferred
outside the if-statement then the file might not have been opened.
However we can extend our original class to cope. I’ll skip the templates for simplicity:
class Deferred { public: using Function = std::function<void()>; Deferred() = default; Deferred(const Function& function) { Add(function) } ... ~Deferred() { for (const auto& function : std::ranges::reverse(m_Functions)) { function(); } } void Add(const Function& function) { m_Functions.push_back(function); } ... private: std::vector<Function> m_Functions; };
This means we can easily cope with the nested blocks:
Deferred deferred; FILE* file = nullptr; if (useFile) { file = fopen(filename, "r"); deferred.Add(fclose, file); } // Do other things. ...
This means a single Deferred
variable can clean up any number of things at the end of the scope.
One thing to watch for is the lifetime of any variables you are cleaning up. Any variables declared after deferred
will have their own destructor called first. That doesn’t matter in this instance, file
doesn’t have a destructor. However it were stream
instead then it would have a destructor.
In the end
This isn’t just limited to nested blocks. It’s suitable for any clean up you want to do later, as long as Deferred
can have the appropriate lifetime. I suspect it’s possible to overuse this. If you can manage to have a simple constructor and destructor in your classes then that’s better, fewer layers of code to understand. This is probably most useful when you can’t make a simple destructor, such as interfacing with external systems.
Leave a Reply