What is RAII?

Matt Lim
The Startup
Published in
6 min readFeb 25, 2020

--

RAII stands for “resource acquisition is initialization”, an acronym (well, actually an initialism, but who’s counting) that’s only useful if you already understand what RAII is. Our goal is to gain that understanding. In this post, I’ll first talk about the problems RAII solves. Next, I’ll explain what RAII is, and how it solves those problems. Finally, I’ll show you some examples. Note that I’ll be talking RAII in the context of C++, which is the language it’s usually associated with. However, it’s not language specific, and is also used in several other languages.

If you’d rather watch a video about this material, check out the accompanying YouTube video here.

What Problems Does It Solve?

Let’s say I want to dynamically initialize an array. For some reason, I forget that std::vector exists, so I write int *arr = new int[dynamicSize];. This is all good, except that if I forget to write delete[] arr; I’ll have a memory leak. In other words, I have to explicitly manage my memory if I want to avoid leaks.

Now, let’s think about locks. Let’s say I want to explicitly synchronize some section of my code. I do this by creating a std::mutex and then calling mutex.lock(). This works fine, except that if I forget to call mutex.unlock(), or if an exception occurs before the unlock, my other thread will deadlock when it tries to obtain the lock.

Finally, what about threads? Let’s say I want to run some code in another thread, and so I create one. This works fine as long as I remember to join the thread; if I don’t, and the thread goes out of scope, then std::terminate will be called and my program will end.

If you’ve been paying attention, you might’ve noticed that all these problems follow a particular pattern. Let’s write some code to make it a bit more clear.

Do you see the pattern? In each example, we begin by acquiring a resource: in the first example, we acquire memory on the heap; in the second example, we acquire a lock; in the last example, we acquire a thread. In isolation, nothing is wrong with these acquisitions. However, when the resources go out of scope, problems arise. We get memory leaks, deadlocks, and terminations. These problems aren’t impossible to solve; in each case, all we need to do is write the corresponding “unacquire” line of code, i.e. write some cleanup stanzas. However, it is hard to always remember to do this, and even when we do remember, it does not always produce safe code. RAII intends to solve these problems.

Note: Explicitly writing out these cleanup stanzas (e.g. freeing pointers, closing files, etc.) is fine in C, and you’ll often see it. C isn’t object-oriented, and doesn’t support RAII.

What Is Raii?

Simply put, RAII is when you acquire resources in a constructor and release them in the corresponding destructor. That’s it! For example, we could implement a stupid RAII class like this (don’t actually write this):

RaiiClass::RaiiClass(int *ptr) : ptr_(ptr) {}
RaiiClass::~RaiiClass() { delete ptr_; }

“What is a resource?” you might be wondering. Good question! When we’re talking about RAII in C++, a resource refers to something that must be “acquired before use.” Note that resources are in limited supply. For example, heap memory, files, sockets, and mutexes are all resources, and must all be acquired before use. It helps me to think of it as an exchange: for example, a program must ask the operating system for heap memory before using it.

Resource acquisition (simplified, not actually how brk() and sbrk() work)

Acquiring resources in a constructor and releasing them in the corresponding destructor binds resource lifetime to object lifetime, and makes it so that we no longer have to remember to explicitly release resources whenever they’re acquired. For example, instead of having to always write delete ptr; whenever we’re done with some piece of memory, we can put that code into some object’s destructor; the memory will then be freed whenever the object gets destroyed.

The consequences of this are surprisingly big — it’s one of the things that makes writing C++ much different than writing C. For example, a rule of thumb in C++ is that you should never write new or delete. Instead, you should use an RAII object which manages the memory, i.e. you should couple memory allocation and deallocation to object lifetime. This rule is usually easy to follow, since the standard library provides classes like std::vector and std::unique_ptr. If these classes don’t fit your use case, then you can roll your own RAII object, in which case new and delete will be present in the class implementation but not in user code. In general, you should avoid writing cleanup stanzas, and move such code into the destructor of an RAII object. Of course, there are exceptions to these rules, but they’re good to keep in mind.

Note: RAII means you don’t need to worry about resource lifetime. However, you still need to think about object lifetime!

RAII in a nutshell

Examples

It’s time to look at some code! I’m going to use mutexes for the main example. We’ll go over a bad example — what not to do — and how to fix it with RAII. Then we’ll go over a few shorter examples involving threads and pointers.

Note: To follow along with the code, check out https://github.com/arcticmatt/blogtube/tree/master/cpp/raii

Mutex Bad Example

In this example, we manually lock and unlock the mutex. That is, this example does not use RAII, and thus is prone to error.

This is that deadlock situation I was talking about earlier. If you run this code, here’s what happens:

$ clang++ -std=c++17 -o RaiiMutex RaiiMutex.cpp && ./RaiiMutex
Locking mutex manually...
Mutex is locked!
Throwing exception
Caught exception
Locking mutex manually...

It hangs after the last line. This means lockMutexBad is not exception safe; in other words, if an exception occurs in between the lock and unlock, the lock will never be released. It’s also pretty easy to just plain forget to release the lock, for example if there are a bunch of early return statements.

Mutex Good Example #1

This example uses std::scoped_lock, an RAII class for acquiring locks. Specifically, it locks a mutex in its constructor, and unlocks it in its destructor.

Here’s what happens when you run this code:

$ clang++ -std=c++17 -o RaiiMutex RaiiMutex.cpp && ./RaiiMutex
Locking mutex with scoped_lock…
Mutex is locked!
Throwing exception
Caught exception
Locking mutex with scoped_lock…
Mutex is locked!

It no longer hangs! This is because std::scoped_lock is an RAII class, and releases the lock in its destructor. As a result, std::scoped_lock is resilient to exceptions and early returns (i.e. it will be unlocked in these situations).

Mutex Good Example #2

In this example, we implement a simple version of std::scoped_lock so we can get a better look at what’s going on.

Here’s what happens when we run the code:

$ clang++ -std=c++17 -o RaiiMutex RaiiMutex.cpp && ./RaiiMutex
CustomScopedLock locking mutex…
CustomScopedLock has locked mutex!
Throwing exception
CustomScopedLock has unlocked mutex
Caught exception
CustomScopedLock locking mutex…
CustomScopedLock has locked mutex!
CustomScopedLock has unlocked mutex

Note that after the exception gets thrown, CustomScopedLock unlocks the mutex in its destructor. This is exactly what std::scoped_lock was doing, but now our logs are a little clearer and we implemented it ourselves!

Miscellaneous Examples

If you want your threads to be automatically joined based on object lifetime, you can write a simple RAII class like this:

Once C++20 comes out, std::jthread should make these kind of hand-rolled solutions unnecessary.

As far as pointers go, you should never write new — always use a smart pointer, i.e. always use either std::make_unique or std::make_shared. It’s ok to pass pointers around — for example, it’s fine when C++ functions take pointers as arguments — but the memory should be tied to the object lifetime of some smart pointer. This also means you should never write delete, because the memory will be freed when the smart pointer is destroyed. Of course, there are exceptions to these rules, but they should be rare.

I’m not going to list out every RAII class in existence (std::vector instead of arrays, std::string instead of character arrays, etc.), but I don’t think I need to. By now you should get the gist. If you’re writing cleanup stanzas — if you’re writing delete/unlock/close/etc. — you should think carefully about what you’re doing. There’s usually an RAII class you can use instead.

--

--

Matt Lim
The Startup

Software Engineer. Tweeting @pencilflip. Mediocre boulderer, amateur tennis player, terrible at Avalon. https://www.mattlim.me/