Migrating from C to C++: Take Advantage of RAII/SBRM

When I look back at many of the C projects I've contributed to, I find that many of them suffer from a common form of structural clutter. I'm sure you've seen similar patterns:

int foo(char c)
{
    int r = 0;

    my_mutex.lock();
    file_t fhandle = fopen("blah.bin");

    if(fhandle == NULL)
    {
        my_mutex.unlock();
        return -1;
    }

    // Do some stuff
    {...}

    if(some_test)
    {
        //Do some stuff
        {…}

        if(some_other_test)
        {
            //FAIL!
            fclose(fhandle);
            my_mutex.unlock();
            return -1;
        }
    }
    else
    {
        //FAIL!
        fclose(fhandle);
        my_mutex.unlock();
        return -1;
    }

    // Do Some other stuff
    {...}

    //Ok - Exit
    my_mutex.unlock();
    fclose(handle);

    return r;
}

What is a programmer to do? You have to ensure that your mutex is unlocked before you leave a function, or else your program is going to deadlock. You have to release that file when you're done with it, or the next attempt to open that file will result in failure. Similar structural clutter can be seen with dynamic memory allocations or any other operation that you must logically undo.

These "cleanup" requirements are why I (and many other programmers) espouse a single function exit point. Unfortunately, when I do encounter a single exit point, I usually see another negative pattern: using a goto statement to jump to a cleanup section:

int foo(char c)
{
    int r = 0;

    my_mutex.lock();
    file_t fhandle = fopen("blah.bin");

    if(fhandle)
    {
        //Do some stuff
        {...}

        if(some_test)
        {
            //Do some stuff
            {...}

            if(some_other_test)
            {
                //Fail!
                r = -1;
                goto cleanup;
            }
        }
        else
        {
            //Fail!
            r = -1;
            goto cleanup;
        }

        //Do some stuff
        {...}
    }

cleanup:
    if(fhandle)
    {
        fclose(fhandle);
    }

    my_mutex.unlock();

    return r;
}

Structural clutter aside, programmers have to remember to undo specific operations before they exit a function. Anytime I'm required to remember a bunch of details, it increases the chances that I will forget one and introduce a bug into my program. I can't begin to count the number of times I've debugged a problem only to end up finding a resource that wasn't cleaned up properly!

Luckily, C++ gives us a pattern that can help us solve this structural problem: Scope-Bound Resource Management (SBRM). Using SBRM, we can rely on classes which handle their own resource management. Instead of worrying about obtaining and releasing underlying resources, we are able to focus on the higher level logic.

What is SBRM?

SBRM is a programming idiom used in some object-oriented languages such as C++. You will also see SBRM referred to as "Resource Acquisition is Initialization" (RAII). I find that "scope-bound resource management" provides a much clearer meaning than "resource acquisition is initialization", so I will continue to use SBRM in this article.

With SRBM, a class is responsible for acquiring and releasing its underlying resources. Resource allocation/acquisition is handled during construction, and cleaning up said resources is handled during destruction. By utilizing the constructor and destructor we can ensure that the resource is tied to the object's lifetime. As long as we are destructing objects that hold resources, we don't have to worry about leaking those resources.

This example, lifted from the Wikipedia SBRM entry, illustrates SBRM with both a std::lock_guard and a std::ofstream file. Notice that there is no use of the boilerplate mutex.unlock() or file.close() operations.

#include <mutex>
#include <iostream>
#include <string> 
#include <fstream>
#include <stdexcept>

void write_to_file (const std::string & message) {
    // mutex to protect file access (shared across threads)
    static std::mutex mutex;

    // lock mutex before accessing file
    std::lock_guard<std::mutex> lock(mutex);

    // try to open file
    std::ofstream file("example.txt");
    if (!file.is_open())
        throw std::runtime_error("unable to open file");

    // write message to file
    file << message << std::endl;

    // file will be closed 1st when leaving scope (regardless of exception)
    // mutex will be unlocked 2nd (from lock destructor) when leaving
    // scope (regardless of exception)
}

No matter how we leave a function (exception or normal exit), the destructors for objects declared within that scope will always be called, ensuring that we will not leak any objects or forget to clean up any resources. Look for areas in your code where you are manually undoing an operation. You will likely find that SBRM is a viable strategy in those areas.

Benefits of SBRM

In my mind, the primary benefit of utilizing SBRM is encapsulation. Resource management logic is contained within the class itself. This logic is defined and implemented once, rather than at each call site (as in C). Once I create an object, I can trust that it will be cleaned up correctly. I don't have to remember to do anything at all: it just works.

By improving our software's encapsulation we help simplify our code into more manageable units. State management, error recovery, and exception handling for our resource can all be contained in one central location. As the complexity of managing that resource increases, we know that it will increase in a single location rather than throughout our program.

Structurally, SBRM reduces programming overhead and improves readability. We can eliminate the resource acquisition, extra conditional checks, and close/release/free calls from our functions. By separating the acquisition and cleanup from our core logic, our functions become more readable and the functional logic becomes the primary focus.

When utilizing the stack, SBRM also enforces reverse-order clean-up. As long as you are creating objects in the correct order, they will be cleaned up in the correct order. No more risk of releasing your mutex before finishing with the resource you are guarding!

By abstracting away underlying details, we reduce the cognitive load and free up valuable mental space for higher level details. Since we've reduced the set of details that a programmer has to remember, we've increased the chances of our program operating correctly. Our code becomes safer since the compiler is handling the acquisition and cleanup invocations for us, reducing the chances of resources being left dangling.

Common Use Cases

Generally, any resource that you would need to manually clean up can likely be handled with SBRM:

  • Mutexes
  • Synchronization primitives, such as critical sections (commonly found in embedded systems)
  • Files
  • Sockets / network connections
  • Database handles
  • Dynamically allocated memory

Regarding the C++ standard library, classes which manage their own resources follow SBRM principles. For example, you can utilize:

The standard library provides wrappers to enable SBRM concepts:

For C++ examples of SBRM, please refer to the links above. The examples tend to be pretty trivial, since the pattern results in pushing resource management into an object's constructor/destructor.

Implementing your own SBRM-compatible classes is easy: Simply make sure that your constructor and destructor correctly handle acquiring and releasing resources.

Costs and Limitations of SBRM

The primary difficulty with utilizing SBRM is getting ownership right. You must have a clear idea of who should own your object, the desired lifespan, and when ownership needs to be transferred to another object. The lifetime of stack-allocated objects is easily understood, as you can expect destructors to be called once you exit a function. This is not to say that SBRM only works for objects allocated on the stack. I utilize SBRM within classes that are declared on the heap. For example, consider a USB device container: when the USB device is removed, the USB stack deletes the container and the underlying resources are cleaned up correctly.

The runtime costs of utilizing SBRM are minimal. There is little-to-no additional storage overhead for utilizing SBRM instead of manual resource acquisition APIs. You are calling a non-trivial constructor/destructor, but often our constructors and destructors can be inlined by the compiler. Even if your constructor is not inlined, you've simply added the cost of an extra function call. Encapsulating complex behavior into functions is a tradeoff we frequently make elsewhere in our programs.

Debugging code utilizing SBRM can be more frustrating, as you now have to step through your constructors and destructors in order to see the action happening. However, you generally won't find yourself stepping through constructors and destructors once you have verified the correctness of your class.

Make sure the purpose and usage of your SBRM-enabled class are clear and understood. You should be making lives easier, not making your program harder to understand.

Further Reading

If you still have questions about SBRM/RAII, I recommend reading the links below.