Every time I read about a new system-programming language, I’m enthused about the possibility of learning cool concepts. Those languages are designed from scratch by groups of super-capable engineers with an accumulated experience of hundreds of years in order to create the “best” eco-system possible for this kind of programming.
This was exactly the reason that made me learn Go some time ago. Go is a very-cool language, conceived in 2007 at Google and aiming to ease the development process for applications that involve multi-core machines and/or elaborate networks. The goal was to create a clean and simple language while retaining efficiency, static typing and modularity.
One of the things that I liked most about Go, was the
defer concept. I liked it so much that I ran straight back to my compiler and wrote a C++ implementation I use ever since (both for my private projects and at work).
But before we talk about it, let’s first present the problem solves.
Any decent C++ developer knows that RAII is one of the cornerstones of modern C++. And don’t just take my word for it:
When writing RAII-compliant code, you don’t have to worry about the de-allocation of the resource. Take for example
std::ifstream , you’ve created an instance, tried to read some data from the file and whether succeeded or failed miserably, you don’t have to do anything else! No need to close anything, no need to free anything, everything is handled for you!
But(!), when programming in C or C++, many system APIs will hand you a resource, that you are then responsible for closing/deallocating/etc. Understandably, this, just like any other task we have to do manually, is easily forgotten, making it prone to bugs, leaks and what not.
Let’s consider the following example, is there anything wrong with it?
YES, there is! If we manage to
open the file, and then
read_current_startup , but then fail while trying to
write_current_startup , we will never close the
fd . This is of course a resource leak, and may well result in exhausting all the file descriptors on a Linux system, if it goes unnoticed for too long.
So, what are the solutions C++ developers usually use for such cases? They are dime a dozen, but I’ll try and pinpoint two common ones.
C-style solution: Goto
Most C programmers will scream
goto right away. Instead of handling the resource deallocation inside the
if clause, we move the entire logic to the end of the function, and jump to it using
goto. When used mildly, like in our example, it seems rather elegant:
But, will it scale? Let’s see how it looks with more than one tag:
> Note: This is an actual file from the Linux kernel (v4.20)
If you ask me, this is just as error-prone. I’ve seen developers jump to the wrong label multiple times, and the repercussions are just the same. If you ask me, in most cases,
goto is avoidable without compensating style or readability.
C++ style solution: Managed Objects
In other scenarios I’ve seen, responsible developers created a managed wrapper around the specific object, so that it gets freed once it leaves the scope. An (ultra-simplified) managed
fd object will look something like this:
And when we use it, it will look like:
This is actually very neat, but presents a non-negligible overhead for developers, because they need to write many unique wrappers.
This becomes even worse, when our code demands symmetric initialization and finalization of modules by simply calling
Go-style solution: Defer
defer solves exactly that. This keyword simply accepts a function, and it will execute it when the program leaves the current scope. Here is the same example in Go:
As you can see, right after we successfully opened the file, we deferred the
file.Close() operation. This way, no matter what happens, the file will always be closed gracefully and responsibly when the method.
C++ defer implementation
Like I said before, this concept was a eureka moment for me, and I simply had to add this to my toolbox.
I’m really glad to say that I’ve actually made something very similar without any nasty tricks, and here is my final implementation:
The implementation is rather simple. It uses a lambda-expression to create a function object that is executed once the object goes out of scope. And by taking advantage of lambda’s capture group, we are able to use the local members inside the cleanup function.
And how will that look with our original example? As beautiful as it gets: