RAII in C++
Back to Basics: RAII in C++ - Andre Kostur - CppCon 2022
Source: Back to Basics: RAII in C++ - Andre Kostur - CppCon 2022
What is Resource?
- Resource
- is some facility or concept that you gain access to by a statement/expression, and you release or dispose by some other statement/expression.
Loosely, speaking, resource is something you must acquire and later dispose.
Common Resources
| Resource | Acquire | Dispose |
|---|---|---|
| Memory | p = new T; | delete p; |
| POSIX File | fp = fopen("filename", "r"); | fclose(fp); |
| Joinable threads | pthread_create(&p, NULL, fn, NULL); | pthread_join(p, &retVal); |
| Mutex locking | pthread_mutex_lock(&mut); | pthread_mutex_unlock(&mut); |
Use word dispose rather than delete, because talk about resource not memory
Resource Usage Issues
Using mutex as the example resource.
Leak
Early return, exception, forget to unlock.
1
2
3
4
5
6
7
std::mutex m;
void f() {
m.lock(); // acquire
if (...) return; // early return
m.unlock(); // don't run if it returns early
}
Effect: mutex stays locked, causing other threads to block forever (deadlock / hang).
Use-after-disposal
1
2
3
4
5
6
7
8
std::mutex m;
int shared = 0;
void f() {
m.lock(); // acquire
m.unlock(); // disposal
shared++; // shared is not protected anymore
}
After m.unlock(), current thread no longer holds the mutex, so other threads may modify shared concurrently, causing a “data race”.
Double-disposal
Unlocking an unlocked mutex is UB.
1
2
3
4
5
6
7
std::mutex m;
void f() {
m.lock(); // acquire
m.unlock(); // disposal
m.unlock(); // double-disposal
}
What is RAII?
- C++ Object Lifetime
- Objects have a defined beginning of life and end of life, that have code which will automatically run: constructors and destructors.
- Resource Acquisition Is Initialization (RAII)
- an idiom where resource acquisition is done in the constructor of an “RAII class”, and resource disposal is done in the destructor of an “RAII class”.
- Ownership
- An RAII class is said to own the resource. It is responsible for cleaning up (dispose) that resource at the appropriate time.
RAII Example: std::lock_guard
std::lock_guard is the standard RAII class to lock a single mutex during constructor, and unlock it during destructor.
1
2
3
4
5
6
7
8
9
10
11
12
13
bool fn(std::mutex & someMutex, SomeDataSource & src) {
someMutex.lock();
try {
BufferClass buffer;
if (not src.readIntoBuffer(buffer)) {
someMutex.unlock();
return false;
}
buffer.display();
} catch (...) { someMutex.unlock(); throw; }
someMutex.unlock();
return true;
}
The first version manually calls unlock() in multiple paths. This is error-prone: an early return or exception can easily skip an unlock(), leaving the mutex locked (deadlock). It also forces extra try/catch boilerplate just to ensure cleanup.
With RAII, std::lock_guard locks in its constructor and unlocks in its destructor, so cleanup happens automatically on every exit path.
1
2
3
4
5
6
7
8
9
bool fn(std::mutex & someMutex, SomeDataSource & src) {
std::lock_guard lock{someMutex};
BufferClass buffer;
if (not src.readIntoBuffer(buffer)) {
return false;
}
buffer.display();
return true;
}
Storage durations
So far we’ve only talked about automatic storage duration variables.
RAII works with any of the C++ object lifetimes.
1
2
3
4
void SomeClass::fn() {
auto worker{std::jthread{[] { /* do something */ }}};
m_vec.push_back(std::move(worker));
}
worker (an RAII object) “owns” the thread, then you std::move it into m_vec, so now the vector element owns it. When that element dies later, cleanup happens automatically.
RAII Example: std::unique_ptr
std::unique_ptr is the standard RAII class to own a dynamically allocated object alone (using new and delete).
You can’t copy a
std::unique_ptr, you can only move it.
1
2
3
auto p = std::make_unique<int>(5); // create unique_ptr
// auto q = p; // CE: copy not allowed
auto q = std::move(p); // ownership moved
RAII Example: std::shared_ptr
std::shared_ptr represents a reference-counted shared pointer. Object is destroyed only when count hit 0.
You can copy it (adds new owner and increments the reference-count)
1
2
3
4
5
void demo_shared_ptr() {
auto p = std::make_shared<int>(5); // count = 1
auto q = p; // copy => count = 2
std::cout << p.use_count() << "\n"; // 2
} // q and p go out of scope => count hits 0 => int freed automatically
Other Standard RAII classes
std::unique_lock: a more sophisticatedstd::lock_guard, but you can unlock/relock it during its lifespan, and other more sophisticated thingsstd::jthread(C++20): owns a joinable thread, and will automaticallyjoin()during destructionstd::fstream: owns the files
Reclaim Responsibility
- RAII classes may provide ways to get direct access to the enclosed resource.
1
2
3
auto p = std::make_unique<int>(5);
int* raw = p.get(); // just access (peek)
*raw = 10; // ok: using the int
p still owns the int, raw is just a borrowed pointer.
Never
delete rawand make surepstay alive while usingraw(double disposal aspwill automaticallydeleteit).
- RAII classes may even provide ways to break the resource out of the RAII class altogether.
1
2
3
4
auto p = std::make_unique<int>(5);
int* raw = p.release(); // ownership transferred to you
// p is now nullptr
delete raw; // you MUST manually free it
Not a Panacea
There are other failure modes that RAII is not intended to solve:
- Resource loops
1 2 3 4
auto a = std::make_shared<A>(); auto b = std::make_shared<B>(); a->b = b; // A owns B b->a = a; // B owns A
Both
aandbwill never be destroyed because they reference each other, causing a memory leak.Use
std::weak_ptrto break the cycle. - Deadlocks
1 2 3 4 5 6 7 8 9
std::mutex m1, m2; void f() { std::lock_guard lock1 {m1}; std::lock_guard lock2 {m2}; } void g() { std::lock_guard lock1 {m2}; std::lock_guard lock2 {m1}; }
If
f()andg()run concurrently, they can deadlock:f()locksm1,g()locksm2, then both try to lock the other mutex, so both wait forever.Use
std::scoped_lockto lock multiple mutexes at the same time (deadlock-safe).
Implementing RAII Classes
Pointer resource handle
If the resource is represented as a pointer already, then you often don’t need to implement your own RAII class. std::unique_ptr is likely already able to manage the pointer for you.
std::unique_ptr<T>for exclusive ownership of a dynamically allocated object (deleteby default)std::unique_ptr<T, Deleter>if cleanup is notdelete(custom dispose function)Example: C-style file handle
C-style file handles must be closed manually:
1 2
FILE * fopen(const char * filename, const char * mode); int fclose(FILE * stream);
We can wrap FILE* in a std::unique_ptr with a custom deleter.
- Define a deleter (functor)
1 2 3
struct file_closer { void operator()(FILE * stream) const { fclose(stream); } };
- Define the RAII type
1
using cfile = std::unique_ptr<FILE, file_closer>;
Or: in C++20 you can more simply do:
1 2
using cfile = std::unique_ptr<FILE, decltype([](FILE * fp){ fclose(fp); })>;
- Factory function to open files safely
1 2 3 4 5 6 7
auto make_cfile(char const * filename, char const * mode) { FILE * stream{fopen(filename, mode)}; if (not stream) { throw std::runtime_exception{ "Failed to open file" }; } return cfile{stream}; }
- Use the RAII class
1 2 3 4
void fn() { auto file{make_cfile("filename.txt", "w")}; fprintf(file.get(), "Data for the file"); }
Shared resource handle
- Define a deleter (functor)
std::shared_ptr also supports a custom deleter like std::unique_ptr with the added feature that you have a reference-counted resource.
Writing your own RAII class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Canonical RAII wrapper skeleton (used in the sections below)
class Handle {
ResourceType r = nullptr; // empty state
public:
Handle() = default; // default = empty
explicit Handle(ResourceType h) : r(h) {} // adopt existing handle (explicit)
// non-copyable
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
// movable
Handle(Handle&& other) noexcept : r(other.r) { other.r = nullptr; }
Handle& operator=(Handle&& other) noexcept {
if (this != &other) {
reset();
r = other.r;
other.r = nullptr;
}
return *this;
}
void reset() { if (r) { dispose_resource(r); r = nullptr; } } // early release
[[nodiscard]] ResourceType release() { auto tmp = r; r = nullptr; return tmp; }
ResourceType get() const { return r; } // borrow raw handle
~Handle() { reset(); }
};
When writing your own RAII class, there are some design questions that you will need to ask
Is there a valid default acquisition?
- Provide a default constructor to set that up.
- Also allow an empty state (owns no resource).
- Destructor may need to understand that the resource was released “early”.
1
Handle() : r(acquire_default_resource()) {}
Is there a valid “empty” state?
An RAII object may be valid while owning no resource.
- Example:
std::unique_ptr<T>can be empty (nullptr).
- You can make the default constructor create an empty object instead of acquiring anything.
- The destructor must handle the empty state correctly (i.e. do nothing when there is no resource).
1
2
ResourceType r = nullptr; // empty state
Handle() = default; // default = empty
Is adopting a resource allowed?
- Provide a single-parameter constructor (usually
explicit) to adopt the handle. - Does not preclude an empty state as well (e.g. handle =
nullptr/-1). - Destructor may need to understand that the resource was released “early”.
1
explicit Handle(ResourceType h) : r(h) {} // adopt existing handle
Copyable?
(can two objects own the same resource?)
- If not,
= deletethe copy constructor, and copy assignment operator. - Example:
std::shared_ptris copyable,std::unique_ptris not.
1
2
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
Movable?
(transfers ownership, the source becomes empty)
- If not,
= deletethe move constructor and move assignment operator. - Example:
std::shared_ptrandstd::unique_ptrare movable.std::scoped_lockis not.
1
2
3
4
5
// Not movable
Handle(Handle&&) = delete;
Handle& operator=(Handle&&) = delete;
// Movable
Handle(Handle&& other) noexcept : r(other.r) { other.r = nullptr; }
Access underlying representation?
- If yes, provide a
.get()or.native_handle()method to return the raw representation (without transferring ownership). - Example:
std::jthreadprovides.native_handle(), butstd::scoped_lockdoes not.
1
2
3
4
5
6
7
8
9
10
class File {
FILE* fp = nullptr;
public:
explicit File(const char* name) : fp(fopen(name, "w")) {}
~File() { if (fp) fclose(fp); }
FILE* get() const { return fp; } // borrow raw pointer (no ownership transfer)
};
std::fprintf(file.get(), "hello\n"); // use: call a C API that wants FILE*
Hide the underlying representation?
- If yes, provide member functions to expose the functionality desired without exposing the raw representation.
- You may still choose to provide
.get()/.native_handle()for cases you haven’t considered.
1
2
3
4
5
6
class File {
// same as before
void write(const char* data) { std::fprintf(fp, "%s", data); } // hide raw pointer
};
file.write("hello\n"); // use: call the member function instead of C API
Dependent resources
Sometimes acquiring one resource lets you acquire another dependent resource.
In that case, provide an acquisition function that returns another RAII object to manage the dependent resource.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Db {
void begin() { ... }
void commit() { ... }
void rollback(){ ... }
struct Tx { // dependent RAII object
Db& db;
bool done = false;
explicit Tx(Db& db) : db(db) { db.begin(); } // acquire
void commit() { db.commit(); done = true; } // finish
~Tx() { if (!done) db.rollback(); } // dispose (auto rollback)
};
Tx transaction() { return Tx(*this); }
};
Usage:
1
2
3
4
5
6
Db db;
{
auto tx = db.transaction(); // acquire transaction
// do some work
tx.commit(); // finish transaction
} // if commit not called -> rollback() automatically
Release the resource?
- If yes, provide a
.release()method that returns the raw representation and release control (the caller becomes responsible for disposal). - Mark
.release()as[[nodiscard]]so people don’t accidentally ignore the returned raw handle and leak the resource.
1
2
3
4
5
6
7
[[nodiscard]] ResourceType release() {
ResourceType temp = r;
r = nullptr; // mark as empty
return temp; // caller is now responsible for disposal
}
p.release(); // warning if return value ignored
Example RAII class: unique_unlock
1
2
3
4
5
6
7
8
9
10
11
template <class Mutex>
class unique_unlock {
public:
explicit unique_unlock(std::unique_lock<Mutex> & p_lock)
: lock(p_lock) { lock.unlock(); }
// Delete the copy and move constructors and assignment
// operators
~unique_unlock() { lock.lock(); }
private:
std::unique_lock<Mutex> & lock;
};
Usage:
1
2
3
4
5
6
7
8
std::mutex m;
std::unique_lock lock{m}; // lock acquired
// do some work protected by the mutex
{
unique_unlock unlock{lock}; // lock released
// do some work that doesn't need the mutex
}
// do some more work protected by the mutex again
Core Guidelines on Scope
Since RAII and object lifetime are tightly linked, these C++ Core Guidelines are especially relevant:
- R: Resource management: use RAII to manage resources, and avoid manual acquire/dispose.
- ES.5: Keep scopes small: don’t keep resources alive longer than necessary.
- ES.20: Always initialize an object
- ES.21: Don’t introduce a variable (or constant) before you need to use it
- ES.22: Don’t declare a variable until you have a value to initialize it with