Post

RAII in C++

Back to Basics: RAII in C++ - Andre Kostur - CppCon 2022

RAII in C++

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

ResourceAcquireDispose
Memoryp = new T;delete p;
POSIX Filefp = fopen("filename", "r");fclose(fp);
Joinable threadspthread_create(&p, NULL, fn, NULL);pthread_join(p, &retVal);
Mutex lockingpthread_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 sophisticated std::lock_guard, but you can unlock/relock it during its lifespan, and other more sophisticated things
  • std::jthread (C++20): owns a joinable thread, and will automatically join() during destruction
  • std::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 raw and make sure p stay alive while using raw (double disposal as p will automatically delete it).

  • 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 a and b will never be destroyed because they reference each other, causing a memory leak.

    Use std::weak_ptr to 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() and g() run concurrently, they can deadlock: f() locks m1, g() locks m2, then both try to lock the other mutex, so both wait forever.

    Use std::scoped_lock to 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 (delete by default)
  • std::unique_ptr<T, Deleter> if cleanup is not delete (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.

    1. Define a deleter (functor)
      1
      2
      3
      
      struct file_closer {
      void operator()(FILE * stream) const { fclose(stream); }
      };
      
    2. 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); })>;
      
    3. 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};
      }
      
    4. 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

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?

  1. Provide a default constructor to set that up.
  2. Also allow an empty state (owns no resource).
  3. 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).
  1. You can make the default constructor create an empty object instead of acquiring anything.
  2. 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, = delete the copy constructor, and copy assignment operator.
  • Example: std::shared_ptr is copyable, std::unique_ptr is not.
1
2
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;

Movable?

(transfers ownership, the source becomes empty)

  • If not, = delete the move constructor and move assignment operator.
  • Example: std::shared_ptr and std::unique_ptr are movable. std::scoped_lock is 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::jthread provides .native_handle(), but std::scoped_lock does 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
This post is licensed under CC BY 4.0 by the author.