RAII

What is RAII?

RAII stands for ‘Resource Acquisition Is Initialization.’ It is arguably one of the most elegant and useful features that C++ has introduced to the world. Both D and Rust have incorporated it into their specifications as well.

What does it mean?

RAII, “obviously,” means that any entity requesting a resource from the system (memory, file handle, network connection, etc.) should be responsible for releasing that resource when its lifetime has ended.

In C++ jargon, this means that any resource needed by an object must be acquired by the object’s constructor and released in its destructor.

Thanks to this very interesting feature, when a variable representing an object created with value semantics goes out of scope, its destructor is invoked automatically and seamlessly, releasing any resources the object may have acquired during its lifetime.

That solves many resource-related issues in a very transparent way when the variables go out of scope:

  • Any dynamically allocated memory owned by this object can be released.
  • Any open file handle can be closed.
  • Any network connection can be closed.
  • Any database connection can be closed.
  • Any registry handle can be released.
  • Any mutex can be unlocked.

… and so on.

The best part is that, while several languages offer garbage collectors limited to handling memory, RAII provides a cleaner alternative for managing not only memory but any type of resources.

Now, let’s see how it can be used.

First, let’s look at how these constructors and destructors are invoked:

#include <iostream>

class A final
{
    int n;

public:
    explicit A(int n) : n{n} { std::cout << "Hello " << n << std::endl; }
    ~A() { std::cout << "Bye " << n << std::endl; }
};

void test()
{
    A a{1};
    A b{2};
    A c{3};
}

int main()
{
    std::cout << "Begin" << std::endl;
    test();
    std::cout << "End" << std::endl;
}

When running this, the output will be:

Begin
Hello 1
Hello 2
Hello 3
Bye 3
Bye 2
Bye 1
End

Two things can be noticed here:

  1. The destructors are invoked automatically before exiting the test function. Why is that? Because a, b, and c were created in that code block.
  2. The order of destructor calls is the reverse of their creation order.

Since the destructors are invoked automatically, they can leverage this interesting feature (RAII) to free any resources acquired by their code. For example, they could modify class A to store that int value on the heap instead (which is a bad idea, by the way):

class A final
{
    int* pn;

public:
    explicit A(int n) 
    : pn{new int{n}} 
    {
        std::cout << "Hello " << *pn << std::endl; 
    }

    ~A()
    { 
        std::cout << "Bye " << *pn << std::endl; 
        delete pn;
    }
};

Note that resources are acquired in the constructor and released in the destructor.

In this way, the user of class A does not need to worry about the resources it uses.

“Out of scope” also means that if the function ends abruptly or returns prematurely, it will be guaranteed that the destructors of the objects are invoked before control is transferred back to the caller.

This will be tested by adding an exception:

#include <iostream>

class A final
{
    int* pn;

public:
    explicit A(int n) 
    : pn{new int{n}} 
    {
        std::cout << "Hello " << *pn << std::endl; 
    }

    ~A()
    { 
        std::cout << "Bye " << *pn << std::endl; 
        delete pn;
    }
};

void test(int nonzero)
{
    A a{1};
    A b{2};

    if (nonzero == 0)
        throw "Arg cannot be zero";

    A c{3};
}

int main()
{
    std::cout << "Begin" << std::endl;
    try
    {
        test(0);
    }
    catch (const char* e)
    {
        std::cout << e << std::endl;
    }
    std::cout << "End" << std::endl;
}

Note that an exception is thrown after objects a and b are created. When the exception occurs, the function test ends abruptly, but the destructors of a and b will be invoked before entering the catch block.

The destructor of object c is not invoked because the object was not created when the exception occurred.

The same behavior occurs if a function is exited prematurely.

Now, look at class B that has been added to the example:

#include <iostream>

class A final
{
    int* pn;
public:
    explicit A(int n) 
    : pn{new int{n}} 
    {
        std::cout << "Hello " << *pn << std::endl; 
    }

    ~A()
    { 
        std::cout << "Bye " << *pn << std::endl; 
        delete pn;
    }
};

class B final
{
    A a;
    A b;
    
public:
    B(int valueA, int valueB) : a{valueA}, b{valueB} { }
};

void test()
{
    B a { 4, 5};
    B b { 6, 7};
}

int main()
{
    std::cout << "Begin" << std::endl;
    test();
    std::cout << "End" << std::endl;
}

The output is:

Begin
Hello 4
Hello 5
Hello 6
Hello 7
Bye 7
Bye 6
Bye 5
Bye 4
End

Why are the destructors of A called when B objects go out of scope if a destructor for B was not written?

Because when a destructor is not defined, one is automatically generated by the compiler that invokes the destructors of all member variables with value semantics.

Thus, if basic classes handle resources explicitly, the likelihood of needing to acquire or release resources explicitly in constructors or destructors is actually low.

What about pointers?

RAII does not work with raw pointers, so if something like this is declared in a function:

int* array = new int[1024];

nothing will happen when that variable array goes out of scope.

Is there any way to have pointers handled by RAII?

YES! Through smart pointers!

Other non-memory related uses?

  • std::ifstream and std::ofstream close automatically the file they opened to be read or written.
  • std::lock_guard<T> locks a mutex in its constructor and unlocks it in its destructor, avoiding threads locked by mistake.
  • If UI is being written, a MouseRestorer could be needed that automatically sets the mouse to its default value after it has been changed to an hourglass during a time-consuming piece of code.

Leave a comment