ICS 45C Fall 2021
Notes and Examples: RAII

Includes a code example with the moniker RAII


Background

In our previous example, we saw that C++ provides a feature called exceptions, which explicitly separates the notion of failure from the notion of success, by providing an alternative mechanism for functions to indicate that they've failed. Rather than returning a value, functions are said to throw exceptions when they fail. Calling functions then have the option to either catch those exceptions, or to step aside and let them propagate outward (i.e., the called function's failure is also the calling function's failure) to whoever called them. Ultimately, either one of the active functions on the run-time stack catches the exception and completes its own task, or none of them catches the exception and the program terminates.

At first blush, exceptions are a relatively straightforward concept: Functions either succeed (returning a value) or fail (throwing an exception), and how you handle each kind of outcome is separate. However, we quickly discovered that things are more complicated in C++ than they are in some other programming languages that offer a similar feature. Of particular concern in C++ is the problem of manual management of memory and resources. For example, consider the following straightforward function that dynamically allocates a couple of objects, uses them, and then deallocates them.

void doTheJob()
{
    A* a = buildA();   // Assume this dynamically allocates an A
    B* b = buildB();   // Assume this dynamically allocates a B

    doThings(a, b);
    doMoreThings(a);
    doYetMoreThings(b);

    delete b;
    delete a;
}

An initial reading through this function makes it appear that memory leaks aren't possible, since there only appears to be one path through the code; there are no if statements or loops, so control flow appears to be straight-line. Of course, having learned about exceptions already has tuned you into additional possibilities; this function has anything but straight-line control flow! Consider these scenarios:

Let's assume that the doTheJob function can't be considered to have succeeded if any of the functions it calls fail; a failure of any of these functions means that doTheJob has failed, as well. (More often than not in real designs, this is the case. We generally catch exceptions in many fewer places than you might imagine.) In that case, we don't want this function to catch exceptions and fully handle them, yet we also have to ensure that we don't leak memory here. These needs lead to the following, rather contorted-looking logic:

void doTheJob()
{
    A* a = nullptr;
    B* b = nullptr;

    try
    {
        a = buildA();
        b = buildB();

        doThings(a, b);
        doMoreThings(a);
        doYetMoreThings(b);

        delete b;
        delete a;
    } 
    catch (...)
    {
        // It's safe to pass nullptr to delete, so we don't technically
        // need to check for nullptr here.
        delete b;
        delete a;

        // Re-throw the exception, since we haven't handled it; we've just made
        // sure to do some cleaning up before our function fails.
        throw;
    }
}

Even leaving aside the comments that I added to explain the design, look how much noise was introduced into what was previously a much simpler example! Consider, too, how error-prone this kind of code is. What do you think the odds are that you could write a 10,000 line program using techniques like these without making mistakes?

Memory isn't the only resource that leaks

There are resources that we manage in C++ other than just memory. Just as there is a "matched pair" of operations to acquire and release memory (e.g., new and delete, to acquire and release dynamically-allocated memory), it's quite common for us to have a "matched pair" of operations to acquire other kinds of resources:

Without some technique for managing resources like these, we end up having to write logic every bit as contorted as the catch-and-rethrow version of the doTheJob function above, in case an exception is thrown that prevents us from releasing a resource when we're finished with it. And if we need more than one kind of resource, that can get quite messy indeed. Consider this hypothetical function that orchestrates the download of a file, ensuring that only one thread downloads at a time. (None of the "library" functions demonstrated below actually exist, but imagine they do.)

void downloadFile(const std::string& host, unsigned int port, const std::string& filename)
{
    Lock* lock = acquireLock();
    Connection* connection = connect(host, port);
    File* file = openFile(filename);

    while (connection->hasMoreData())
    {
        file->writeLine(connection->readLine());
    }

    file->close();
    delete file;

    connection->close();
    delete connection;

    lock->release();
    delete lock;
}

Let's suppose that any of the functions called above can throw an exception except the close()/release() member functions and destructors. Again, let's assume that any failure along the way indicates that the downloadFile function has also failed. If so, then we've got some work to do; the version of this function above will fail to close files, close connections, and release locks. To fix the problem, we'll need to introduce some additional noise:

void downloadFile(const std::string& host, unsigned int port, const std::string& filename)
{
    Lock* lock = nullptr;
    Connection* connection = nullptr;
    File* file = nullptr;

    try
    {
        lock = acquireLock();
        connection = connection(host, port);
        file = openFile(filename);

        while (connection->hasMoreData())
        {
            file->writeLine(connection->readLine());
        }

        file->close();
        delete file;

        connection->close();
        delete connection;

        lock->release();
        delete lock;
    }
    catch (...)
    {
        if (file != nullptr)
        {
            file->close();
            delete file;
        }

        if (connection != nullptr)
        {
            connection->close();
            delete connection;
        }

        if (lock != nullptr)
        {
            lock->release();
            delete lock;
        }

        throw;
    }
}

As if this isn't bad enough, imagine we instead wanted to handle partial results, recover and continue on some kinds of failures, and so on. Suddenly, what was once simple, straight-line logic will turn into a mess.

All hope isn't lost, but we need to consider the fact that exceptions introduce a major issue into our designs: We may end up bailing out of functions in arbitrary places, even when we don't expect it, so we need to figure out a design that doesn't require us to consider every possibility and then manually write code to handle it. Fortunately, this idea has been well-considered over the years, and a pattern has emerged for solving it.


Resource acquisition is initialization (RAII)

It's not always the case that resource management is problematic in the presence of exceptions. For example, consider this short function that stores data in a std::vector and returns it.

std::vector<int> getFunctionValues(int n, std::function<int(int)> f)
{
    std::vector<int> v;

    for (int i = 0; i < n; i++)
    {
        v.push_back(f(i));
    }

    return v;
}

There are a few ways that this function could fail with an exception:

Yet there's actually no risk here of a memory leak, even if one of those functions throws an exception. And we don't have to add any additional code to handle these scenarios; the right thing will happen automatically.

As we've seen, C++ provides a natural mechanism to handle "matched pairs" of operations where we acquire some resource (such as dynamically-allocated memory) and then subsequently need to release it. When objects are allocated statically, their constructors are called; when they fall out of scope (even if an exception has been thrown), their destructors are guaranteed to be called. In fact, this is guaranteed even in cases where exceptions are thrown: If a statically-allocated object is constructed successfully before an exception is thrown, it will be destroyed automatically in every case; if not, it won't (and it won't need to be).

This leads to a very important idea that underlies C++ design: Resource acquisition is initialization, an idea so important that C++ programmers often just refer to it by an acronym (RAII). The basic idea is this:

What makes the std::vector example so much simpler than the others is that std::vector adheres to this principle. It acquires a dynamic resource (dynamically-allocated memory) in its constructor, and it automatically deletes it in its destructor. If we successfully finish the line where we initialize v, we're guaranteed that v's destructor will be called, whether because we reached the return statement or because an exception was thrown at any point. It's all automatic, so we don't have to clutter our code with additional logic to ensure this behavior. A std::vector, in short, is a dynamically-allocated array that supports RAII, which makes it easy to use in scenarios where exceptions might be thrown.

The same principle applies within constructors. The first thing a constructor does is to construct an object's member variables, one by one. If one of those constructors throws an exception, we're guaranteed that the destructors will be called only on the member variables that were constructed successfully. So if those member variables automatically clean up any resources they need, we won't have to worry about the constructor leaking those resources when an exception is thrown.

So, in short, we should prefer to move dynamic resource acquisition of all kinds — dynamic memory allocation, files, network connections, locks, and son — into constructors, and then release them in the corresponding destructors. That's it. As is often the case, there are some details that can be tricky, but the idea is quite simple.


Smart pointers

Pointers to dynamically-allocated memory have been a thorn in our side all quarter, because they're a fairly error-prone construct. It's quite easy to use new and forget to write the corresponding delete. It's also easy to get it right the first time, but then adjust your design later in a way that breaks that correspondence. And, of course, now that we've considered how exceptions interplay with these techniques, we're freshly reminded of how difficult it can be to get them right, particularly in a large, complex program.

However, we've seen that there are times that we simply can't avoid dynamic allocation:

What we'd like, though, is to gain the benefits of an RAII-based design, while still retaining the ability to perform dynamic allocation and to have pointers of one type pointing to objects of a different type. The solution to that problem is to use smart pointers. What makes a smart pointer so smart is that it makes an assumption about how we want our memory managed, so that it knows precisely the circumstances that would cause the memory to no longer be needed. Then, under those assumptions, it deletes the memory for us automatically. One very good way to solve that problem would be to do this:

There are a few wrinkles that we'll have to get right. One of them is to consider the effect of copying one of our smart pointers (e.g., passing it by value to a function). If a smart pointer implies unique ownership, and it feels free to destroy the object it points to when it dies, copying is a catastrophic operation to allow. So we should disallow it altogether; it shouldn't be possible to copy-construct or copy-assign one of our smart pointers, so we'll make it explicitly illegal to do so.

Another problem is making the smart pointer "feel" like a pointer, so we can still use operators like * and -> to access the object it points to. The solution to that problem lies in operator overloading. Just as we've previously overloaded the = operator, we can similarly overload other operators, including (unary) * and ->.

This example pulls these details together into an implementation of a class called SmartIntPtr, which implements a smart pointer to a dynamically-allocated integer. (Using templates, a technique we'll see in a later example, you could even write this once and have it include support for any kind of object, rather than just an int. But the core ideas would still be the same.)

The smart pointers built into the C++ Standard Library

I should point out that there are a few kinds of smart pointers built into the C++ Standard Library, and that you would rarely want to build your own as we've done here, unless (a) you want to learn how to do it, or (b) you need ownership semantics that are different from those provided by the built-in ones. If you want to learn more about this, check out std::unique_ptr, std::shared_ptr, and std::weak_ptr in the C++ Standard Library. The smart pointer that we've implemented in this example is conceptually similar to std::unique_ptr<int>.


The code

The official moniker for this code example is RAII, so your best bet is to do this:

Alternatively, you can click the link to the tarball below: