RAII, or Resource Acquisition is Initialization, is a programming idiom that originally came out of the C++ community. However, it’s a powerful approach that can be used in many languages, including old-fashioned C.
In this blog post, I’ll introduce the RAII concept and examine the guarantees it provides in C++. A subsequent post will demonstrate a way to do RAII in C that makes the same guarantees.
RAII was named by Bjarne Stroustrup, the father of C++. In his 1994 book The Design and Evolution of C++, he introduced the concept, to help the programmer “ensure that the resource is correctly released upon exit even if an exception occurs.”
The main idea behind RAII is to bind a resource (such as a file descriptor, a UART port, or so forth) to an object. The resource is held for exactly as long as the object exists. Therefore, as long as the object is valid, the resource is usable, and when the object is destroyed, the resource is released.
The concept is particularly powerful because if offers a way to ensure resources don’t leak when exceptions are triggered. That’s a productive guarantee that allows developers to reason about the validity of their code.
RAII is a structural idiom. Following this idiom, the structure of your code provides a number of guarantees.
Exception-safe resource management: Exceptions (in any language that has them) can cause the program to skip normal user-code. Thrown exceptions back out of functions, and, if provided, through matching try-catch blocks, in a process known as unwinding. The compiler-generated unwinding code guarantees calls to destructors on objects that are going out-of-scope. RAII piggybacks on the guaranteed call to the object’s destructor to ensure that the resource is released or destroyed when it needs to be.
These are structural guarantees: When you properly implement RAII, your code is guaranteed to handle resources correctly. Further, this correctness can be validated through inspection of your code.
As discussed below, there are a number of ways to write the constructor and destructor to achieve RAII. The right choice depends on the resource and the needs of the program.
All of the guarantees provided by RAII are set up during initialization, in the constructor.
When designing your constructor, take into account the dynamic contours of your resource. Generally, the way you setup your constructor will fall into one of three cases:
I’ll discuss each of these cases below.
In this first case, the resource is already allocated or in existence before the constructor is called. Our RAII object keeps an accounting and wraps access, but doesn’t allocate the resource. Ownership of the resource is given to the RAII object, which, depending on the implementation, may be responsible for destroying the resource, or simply relinquishing ownership upon destruction.
Example 1.1: Fixed hardware resources, such as access to a UART, can be managed by passing a reference into the constructor. In this case, the resource becomes owned and managed by the RAII-object. All access to the UART is done through the object. The destructor relinquishes ownership.
Example 1.2: Dynamic message buffers may be passed to your program from the OS, a callback, or other external entity. Pass ownership of the message into the constructor of an RAII-object. All access to the message should be through this object. The destructor releases the message.
This approach makes sense when you are managing ownership and access, and not necessarily allocating or acquiring resources yourself.
In the second case, the resource is allocated inside the constructor. An exception is thrown if the allocation fails. Once allocated, the resource is guaranteed usable until our destructor frees the resource.
Example 2.1: Allocate a block of memory, such as an audio frame buffer. When you need to allocate a block of memory for your program to operate, and this allocation should never fail, you may choose to make the allocation in the constructor. Professional programming dictates you check to make sure the allocation was successful, and that you throw an exception in the unlikely event that the allocation fails. Your destructor frees the allocation.
Example 2.2: Open a file or resource that should never fail. You may choose to open a needed configuration file in your constructor. Although there may be many reasons to believe this file will be successfully opened, you still must check the result of the file-open command. If the file-open fails, and there is no in-constructor workaround, you must throw an exception. Your destructor closes the file.
Example 2.3: The canonical case here is std::lock_guard<mutex>. The constructor for the lock_guard, which is an RAII-object takes an already-instantiated mutex object and calls mutex-lock. This call may or may not block, but it cannot fail. The lock_guard’s destructor calls mutex-unlock.
This approach makes sense when you need to allocate or acquire a resource, and the allocation or acquisition should never fail. If your constructor fails, you must throw an exception.
When implementing this case, take into account that destructors will not be called on objects that throw exceptions during their construction. Destructors are only called on objects that have been completely constructed.
In the third case, the reference to the resource is cleared in the constructor. Resource allocation is done later. This is best for resources that are dynamic or transient.
Example 3.1: When you open a file on an external drive, you must take care. First off, the initial file-open may fail, and another attempt may be made later. Second, if the file opens successfully, the external drive may be disconnected at any time, and the file reference lost. We handle this transient nature by clearing the reference in the constructor and moving all the opening, closing, and reopening code into the object’s methods. All access to the resource first checks the validity of the resource. The destructor must check the reference before closing it.
Example 3.2: There is no guarantee when it comes to opening network connections. Even if successfully opened, a connection may drop at any time for any number of reasons. Handle this by simply clearing the reference to the connection in the constructor. All the opening and connection-handling code should be written in the RAII-object’s methods, outside the constructor and handle failures. All access to the connection must first check for a valid connection. The destructor must check the reference to the connection before closing it.
Example 3.3: As all allocations may fail, code that allocates buffers can be safely written this way. Clear the references to allocations in the constructor. Attempt the allocations later, outside the constructor and handle failures in an appropriate way. The destructor must check each reference, and free the allocated resources.
This approach makes sense for most dynamic and transient resources. RAII objects built with this approach are flexible and responsive to the dynamic nature of their resources. Likewise, program code that uses these objects must handle the additional cases.
The destructor follows through on the design choices you’ve made for your constructor. Write the destructor to cleanly handle the release, destruction, or accounting of the resource. The key is to balance the work done in the constructor.
Looking at the three types of constructors, here are considerations for the destructors:
In the first case, the resource was pre-allocated and passed into our object’s constructor, we need to look to the ownership of the resource to determine how to return ownership.
Here are some possibilities:
In this case, the reference to the resource is guaranteed to be valid in the destructor as it was pre-allocated or is fixed.
In the second case, the resource was allocated in the constructor and we need to free that resource in the destructor. Here are some possibilities:
The reference to the resource is guaranteed to be valid in the destructor. If the constructor threw an exception, this destructor will not be called.
The third case handles resources that are allocated after the constructor. This case also handles dynamic and transient resources acquired during the life of the object.
In this case, the constructor cleared the reference and did not allocate the resource. When the destructor runs, the reference will be valid if the resource was allocated, and remains allocated. Our destructor must check and free the resource if the reference is valid.
For example, in an RAII-object that holds a file reference, the constructor clears the reference. If a file was opened during program operation, and remains open, the reference will be set, and the destructor must close the file.
This is a powerful and simple case which many programmers likely consider standard practice.
RAII is an important C++ idiom for resource management. Notably, RAII provides a structural idiom for proper resource management with exceptions.
The power of the idiom is in the guarantees it provides. Properly used, the destructor for your RAII-object is guaranteed to be called to allow you to free resources.
In a future post, I’ll look at how to make the same guarantees in C.