While I have previously enumerated various reasons for my preference for C++, in this post I am focusing entirely on the C++ RAII idiom, what it means, and a sampling of RAII inspired ideas you may want to use in your next C++ software project.
What is RAII?
In brief: RAII stands for Resource Acquisition Is Initialization and sometimes also referred to as Resource Allocation Is Initialization. As acronyms in our business go, RAII is not particularly memorable. Fundamentally RAII is a C++ design pattern where we start or acquire “something” in the constructor of a class and then complete or release that same “something” in the destructor. With this idiom we rely on the C++ standard’s well defined and explicit object lifecycle. Thankfully, there are plenty of resources to help us understand this idiom. Yet before we can fully grasp the RAII pattern we must ensure an understanding of the C++ object lifecycle. Feel free to skip the next section if you already have a fundamental understanding of C++ constructors, destructors, and how and when the compiler ensures their execution.
C++ Object Lifecycle
It feels a bit presumptuous of me to write even a brief tutorial on such a core C++ topic. However, the C++ object lifecycle is what enables the RAII idiom, so please bear with me.
No, our C++ compiler doesn’t zap electricity into a chunk of dead RAM and scream maniacally to all the world that the object is now alive. Or does it?
Before answering that question we must first understand the “scope” of a variable in C++. Simply put, the scope of a variable and hence the object declared by the variable begins where the variable is declared and ends when the variable can no longer be referenced in the code, typically the next closing brace ( } ) below the variable. Understanding scope is critical to understanding the object lifecycle as well as understanding and correctly creating and using classes facilitating the RAII idiom. For example, the “myItem” object in the code snippet below has scope, and therefore life, only within the enclosing if statement while conversely the “topLevelItem” object has life for the duration of the main() function.
So, back to the topic of “It’s Alive”. The object declared by the variable “topLevelItem” is brought to life when it enters scope at the start of the main() function where stack memory is assigned to the object and after the compiler has executed the corresponding SomeClass constructor. The same is noted for the object “myItem”, which only begins it’s lifecycle if execution flow enters the “if (someLogic)” statement shown. If the execution flow enters the if statement, then we may state that there are two SomeClass objects “alive”. However, if execution flow never enters the “if” statement body shown, then the execution of the main() function will only create a single SomeClass object.
Now, how do these “living” objects come to die, to cease to be?
Sorry, the Catholic in me is coming out today. As with all “lifecycles” there is an end to the life of an object (and our own) and we must always keep this end in mind. The end of an object’s lifecycle is as critical to the RAII pattern as the start of the lifecycle.
Fundamentally an object’s lifecycle ends when the associated variable’s scope ends. It is generally that easy. Object’s allocated “on the heap” using “new” begin their lifecycle with “new” and end their life cycle when the software explicitly “delete”s the object.
When an object’s lifecycle ends, the compiler guarantees the associated class’s destructor will be executed. Simple. To illustrate the lifecycle of a stack allocated object, please review the following animation.
That covers the basics for a stack allocated object, which is our primary use case for create RAII facilitating classes. Keep reading for real examples of classes created to facilitate this useful pattern.
The following RAII ideas and examples should be treated as motivational examples. That being said, I have used equivalents of each of these in production software.
Lock/unlock a mutex
In nearly every embedded software project I have worked upon the software utilized either a Real-Time OS (RTOS) or one of a variety of embedded Linux derivatives. And for better or worse each of these projects required threads and sometimes more threads than I want to admit to. With threads there is oftentimes a need for mutual exclusion to protect access to shared resources. Enter the mutex and enter my very first exposure to RAII and perhaps my number one reason for using C++ in my embedded software projects. Let us analyze the following RAII style enabling class based upon the FreeRTOS recursive mutex.
An inspection (ignoring the less-than-ideal naming conventions used by FreeRTOS) of the FreeRtosScopedLock class shown above reveals the following key RAII points:
- The class constructor locks the mutex.
- i.e a FreeRtosScopedLock object takes ownership of a mutex during construction.
- The class destructor releases (unlocks) the mutex.
- i.e. when a FreeRtosScopedLock object is destroyed, it releases ownership of the same mutex.
Given this example, how would we use it? The following examples illustrate how some software may appear before applying an RAII pattern versus after applying this pattern.
Key points to observe in our before/after code snippets:
- A single line of code replaces four lines of code.
- The single line of code GUARANTEES the desired behavior: i.e the mutex is locked for the duration of the function and guaranteed, by the compiler, to be released upon any return from the function.
- The change to RAII reduced our code maintenance risk. Future maintainers of this function will not “forget” to unlock the mutex if an additional function return point is added.
And it just gets even better. After reviewing the above benefits, the next question of a typical firmware engineer might be regarding the impact on the firmware code size and the performance impacts of “yet another class” being introduced to the project. In this particular example, we can show that the introduction of the RAII class has zero impact on code size and performance. Yes, zero impact. Here is a screenshot of the proof from godbolt.org.
Click here to play with the example yourself.
An inspection of the compiler’s output reveals code that would be exactly the same as if we were writing the function in C: i.e. direct calls to the underlying FreeRTOS APIs at the exact same points in the function we would be required to manually write the code. Between the compiler enforced C++ object lifecycle and compiler optimizations we come to an ideal conclusion: this RAII class and associated usage will reduce the likelihood of human coding errors AND give us the exact same performance as if we had hand-written the various mutex calls ourselves. Win. Win.
Keeping reading for more RAII examples. I promise more code and less verbiage for the remainder of this post.
Need to determine the execution time of a particular scope of code? Nearly every project requires this measurement at some point and to that end I have created various versions of this class. Dig in:
In this RAII style class the “resource” we are tracking across an object lifespan is time. We see the following:
- Recording the current timestamp in the class constructor.
- Grabbing a new timestamp in the class destructor, performing the appropriate math, and recording the measurement results in some manner.
With a class similar to the above, we can now quickly measure scopes of code by simply adding a single line of code to the target code and executing the code to view the results. This RAII style class is great to have on hand in many firmware projects where performance measurement tools may not be available.
See the first example for the same concept with mutexes. In this case however, we apply the RAII pattern to the concept of “critical sections.” Critical sections are commonly needed in firmware to disable hardware interrupts for some small portion of code, referred to as a “critical section.” Dig in:
All the points expressed in our previous mutex example apply to this example.
I recently helped deliver firmware for an IoT project using the lwIP library. As part of that project I developed a generic “server” component which would manage incoming UDP packets and route the packets to appropriate end-point handlers. As the architect and designer of this particular component I wanted to ensure that the UDP packets, allocated from a memory pool by lwIP, were properly released when a handler completed processing the packet. RAII to the rescue! The following is a simplified example:
In this example, the RAII class simply records the packets pointer value, as it was already allocated by the underlying lwIP library. Then, as is normal with the RAII idiom, the class’s destructor releases the resource in question. With this class in hand, the generic packet handlers received a const object reference to a ProtocolDataUnit class, making it very clear that they were allowed to peek, but not destroy, the received packet.
The C++ or newer standard library provides for various examples of the RAII pattern. One such example from the C++11 standard is std::unique_ptr. A std::unique_ptr the RAII manages a heap allocated object using the RAII idiom, as demonstrated in the following example:
Firmware engineers rightly avoid heap usage, so may never use a unique_ptr. That being said, being aware of RAII examples from the standard library may only serve to further inspire and educate engineers on this valuable C++ pattern. While you are learning about std::unique_ptr, make sure to learn about the newer C++14 std::make_unique.
You never known when an RAII style class will serve your code for the better. If you find yourself writing code and thinking “I need to remember to (release|close|free|complete) this (resource|memory|device|other)” then perhaps the code is calling out for an RAII style class to help aid our collective memory and ensure the task is completed as desired.
Thank you for reading this post, and please let us know in the comments if there is an example that has served your projects!
Could your team use targeted advice like this? If so, please consider our services.