To better understand how RAII works, we must first examine how a class in C++ works as C++ classes are used to implement RAII. Let's look at a simple example. C++ classes provide support for both constructors and destructors as follows:
#include <iostream>
#include <stdexcept>
class the_answer
{
public:
the_answer()
{
std::cout << "The answer is: ";
}
~the_answer()
{
std::cout << "42\n";
}
};
int main(void)
{
the_answer is;
return 0;
}
This results in the following when compiled and executed:
In the preceding example, we create a class with both a constructor and a destructor. When we create an instance of the class, the constructor is called, and, when the instance of the class loses scope, the class is destroyed. This is a simple C++ pattern that has been around since the initial versions of C++ were created by Bjarne Stroustrup. Under the hood, the compiler calls a construction function when the class is first instantiated, but, more importantly, the compiler has to inject code into the program that executes the destruction function when the instantiation of the class loses scope. The important thing to understand here is that this additional logic is inserted into the program automatically by the compiler for the programmer.
Before the introduction of the classes, the programmer had to add construction and destruction logic to the program manually, and, while construction is a fairly simple thing to get right, destruction is not. A classic example of this type of issue in C is storing a file handle. The programmer will add a call to an open() function to open the file handle and, when the file is done, will add a call to close() to close the file handle, forgetting to execute the close() function on all possible error cases that might crop up. This is inclusive of when the code is hundreds of lines long and someone new to the program adds another error case, forgetting also to call close() as needed.
RAII solves this issue by ensuring that, once the class loses scope, the resource that was acquired is released, no matter what the control-flow path was. Let's look at the following example:
#include <iostream>
#include <stdexcept>
class the_answer
{
public:
int *answer{};
the_answer() :
answer{new int}
{
*answer = 42;
}
~the_answer()
{
std::cout << "The answer is: " << *answer << '\n';
delete answer;
}
};
int main(void)
{
the_answer is;
if (*is.answer == 42) {
return 0;
}
return 1;
}
In this example, we allocate an integer and initialize it in the constructor of a class. The important thing to notice here is that we do not need to check for nullptr from the new operator. This is because the new operator will throw an exception if the memory allocation fails. If this occurs, not only will the rest of the constructor not be executed, but the object itself will not be constructed. This means if the constructor successfully executed, you know that the instance of the class is in a valid state and actually contains a resource that will be destroyed when the instance of the class loses scope
The destructor of the class then outputs to stdout and deletes the previously allocated memory. The important thing to understand here is that, no matter what control path the code takes, this resource will be released when the instance of the class loses scope. The programmer only needs to worry about the lifetime of the class.
This idea that the lifetime of the resource is directly tied to the lifetime of the object that allocated the resource is important as it solves a complicated issue for the control flow of a program in the presence of C++ exceptions. Let's look at the following example:
#include <iostream>
#include <stdexcept>
class the_answer
{
public:
int *answer{};
the_answer() :
answer{new int}
{
*answer = 43;
}
~the_answer()
{
std::cout << "The answer is not: " << *answer << '\n';
delete answer;
}
};
void foo()
{
the_answer is;
if (*is.answer == 42) {
return;
}
throw std::runtime_error("");
}
int main(void)
{
try {
foo();
}
catch(...)
{ }
return 0;
}
In this example, we create the same class as the previous example, but, in our foo() function, we throw an exception. The foo() function, however, doesn't need to catch this exception to ensure that the memory allocated is properly freed. Instead, the destructor handles this for us. In C++, many functions might throw and, without RAII, every single function that could throw would need to be wrapped in a try/catch block to ensure that any resources that were allocated are properly freed. We, in fact, see this pattern a lot in C code, especially in kernel-level programming where goto statements are used to ensure that, within a function, if an error occurs, the function can properly unwind itself to release any resources might have previously been acquired. This result is a nest of code dedicated to checking the result of every function call within the program and the logic needed to properly handle the error.
With this type of programming model, it's no wonder that resource leaks are so common in C. RAII combined with C++ exceptions remove the need for this error-prone logic, resulting in code that is less likely to leak resources.
How RAII is handled in the presence of C++ exceptions is outside the scope of this book as it requires a deeper dive into how C++ exception support is implemented. The important thing to remember is that C++ exceptions are faster than checking the return value of a function for an error (as C++ exceptions are implemented using a no overhead algorithm) but are slow when an actual exception is thrown (as the program has to unwind the stack and properly execute each class destructor as needed). For this reason, and others such as maintainability, C++ exceptions should never be used for valid control flow.
Another way that RAII can be used is the finally pattern, which is provided by the C++ Guideline Support Library (GSL). The finally pattern leverages the destructor-only portion of RAII to provide a simple mechanism to perform non-resource-based cleanup when the control flow of a function is complicated or could throw. Consider the following example:
#include <iostream>
#include <stdexcept>
template<typename FUNC>
class finally
{
FUNC m_func;
public:
finally(FUNC func) :
m_func{func}
{ }
~finally()
{
m_func();
}
};
int main(void)
{
auto execute_on_exit = finally{[]{
std::cout << "The answer is: 42\n";
}};
}
In the preceding example, we create a class that is capable of storing a lambda function that is executed when an instance of the finally class loses scope. In this particular case, we output to stdout when the finally class is destroyed. Although this uses a pattern similar to that of RAII, this technically is not RAII as no resource has been acquired.
Also, if a resource does need to be acquired, RAII should be used instead of the finally pattern. The finally pattern, instead, is useful when you are not acquiring a resource but want to execute code when a function returns no matter what control flow path the program takes (a conditional branch or C++ exception).
To demonstrate this, let's look at a more complicated example:
#include <iostream>
#include <stdexcept>
template<typename FUNC>
class finally
{
FUNC m_func;
public:
finally(FUNC func) :
m_func{func}
{ }
~finally()
{
m_func();
}
};
int main(void)
{
try {
auto execute_on_exit = finally{[]{
std::cout << "The answer is: 42\n";
}};
std::cout << "step 1: Collect answers\n";
throw std::runtime_error("???");
std::cout << "step 3: Profit\n";
}
catch (...)
{ }
}
When executed, we get the following:
In the preceding example, we want to ensure that we always output to stdout no matter what the code does. In the middle of execution, we throw an exception, and even though the exception was thrown, our finally code is executed as intended.