Skip to content

RAII: The Fundamental Resource Pattern

Learn RAII (Resource Acquisition Is Initialization) — the core C++ pattern for exception-safe resource management. Implement RAII wrappers, understand lock_guard and scoped_lock, and learn why RAII types must be move-only or ref-counted.

RAII: Acquire in Constructor, Release in Destructor

RAII (Resource Acquisition Is Initialization) is the single most important pattern in C++. The idea is simple:

1. Acquire a resource in the constructor (open a file, lock a mutex, allocate memory, connect to a database).
2. Release the resource in the destructor (close the file, unlock the mutex, free memory, disconnect).

Because C++ guarantees that destructors are called when an object goes out of scope — even if an exception is thrown — RAII makes resource management automatic and exception-safe. You never forget to release, and you never release twice.

Every well-designed C++ type that manages a resource uses RAII: std::string, std::vector, std::unique_ptr, std::fstream, std::lock_guard, std::unique_lock.

Implementing an RAII Wrapper

Here is a complete RAII wrapper for a file descriptor (POSIX). It acquires in the constructor, releases in the destructor, and supports move semantics for ownership transfer.

raii_file_descriptor.cpp
#include <unistd.h>   // open, close, read, write
#include <fcntl.h>    // O_RDONLY, etc.
#include <stdexcept>
#include <utility>
#include <iostream>

class FileDescriptor {
    int fd_ = -1;  // -1 = no resource
public:
    // Acquire: open file in constructor
    explicit FileDescriptor(const char* path, int flags)
        : fd_(::open(path, flags)) {
        if (fd_ < 0) throw std::runtime_error("Cannot open file");
    }

    // Release: close file in destructor
    ~FileDescriptor() {
        if (fd_ >= 0) ::close(fd_);
    }

    // Non-copyable (copying a file descriptor is meaningless)
    FileDescriptor(const FileDescriptor&) = delete;
    FileDescriptor& operator=(const FileDescriptor&) = delete;

    // Moveable (transfer ownership)
    FileDescriptor(FileDescriptor&& other) noexcept
        : fd_(std::exchange(other.fd_, -1)) {}

    FileDescriptor& operator=(FileDescriptor&& other) noexcept {
        if (this != &other) {
            if (fd_ >= 0) ::close(fd_);
            fd_ = std::exchange(other.fd_, -1);
        }
        return *this;
    }

    int get() const { return fd_; }
};

int main() {
    try {
        FileDescriptor fd("/tmp/raii_test.txt", O_RDONLY);
        std::cout << "File opened: fd=" << fd.get() << '\n';
        // Use fd...
    } catch (const std::exception& e) {
        std::cerr << e.what() << '\n';
    }
    // fd.~FileDescriptor() called automatically — file closed
    // Even if an exception was thrown, the file is closed
}

Exception Safety via RAII

Without RAII, exception-safe code requires tedious try/catch blocks for every resource. With RAII, it's automatic — each destructor fires during stack unwinding.

exception_safety.cpp
#include <memory>
#include <mutex>
#include <fstream>
#include <vector>
#include <stdexcept>

std::mutex g_mutex;

// WITHOUT RAII — manual cleanup is error-prone
void bad_example() {
    std::mutex* m = &g_mutex;
    m->lock();
    int* data = new int[100];
    // If this throws, mutex stays locked AND data leaks!
    // process(data);
    delete[] data;
    m->unlock();
}

// WITH RAII — automatic, exception-safe cleanup
void good_example() {
    std::lock_guard<std::mutex> lock(g_mutex);  // RAII lock
    auto data = std::make_unique<int[]>(100);   // RAII allocation
    // If this throws:
    //   1. data's destructor frees the memory
    //   2. lock's destructor unlocks the mutex
    //   Stack unwinding handles everything
    // process(data.get());
}

// Multiple resources — RAII composes naturally
void multi_resource() {
    std::lock_guard<std::mutex> lock(g_mutex);
    auto buffer = std::make_unique<char[]>(4096);
    std::ofstream file("/tmp/output.txt");

    // If any operation throws, ALL resources are released
    // in reverse order of construction:
    //   1. file closed
    //   2. buffer freed
    //   3. mutex unlocked
}

lock_guard, unique_lock & scoped_lock

The standard library provides RAII wrappers for mutexes:

- std::lock_guard — simplest RAII lock. Locks in constructor, unlocks in destructor. Non-moveable, non-copyable. Use when you need a simple scoped lock.

- std::unique_lock — flexible RAII lock. Supports deferred locking, timed locking, manual unlock/relock, and can be moved. Required for use with std::condition_variable.

- std::scoped_lock (C++17) — locks multiple mutexes simultaneously using a deadlock-avoidance algorithm. This is the preferred lock type when locking one or more mutexes.

``cpp
std::mutex m1, m2;
// C++17: lock both without deadlock risk
std::scoped_lock lock(m1, m2);
``

Pitfall

1. Unnamed RAII objects are destroyed immediately:
``cpp
std::lock_guard(mtx); // BUG! Temporary — locks and immediately unlocks
std::lock_guard lock(mtx); // Correct — named, lives until scope end
``

2. Never naively copy RAII types: Copying a file handle or mutex lock makes no sense. RAII types should be:
- Move-only (like unique_ptr, unique_lock) for exclusive ownership, or
- Reference-counted (like shared_ptr) for shared ownership.

3. Destructor should not throw: If a destructor throws during stack unwinding (another exception is active), std::terminate is called. Destructors are noexcept by default in C++11+.

Best Practice

1. Every resource should be owned by an RAII object — memory, files, locks, sockets, database connections, GPU handles.
2. One resource per RAII class — keep wrappers focused.
3. Delete copy operations unless copying the resource is meaningful.
4. Implement move operations to enable ownership transfer.
5. Use std::exchange in move operations — it atomically reads and resets the source: fd_ = std::exchange(other.fd_, -1);
6. Prefer standard RAII types (unique_ptr, lock_guard, fstream) — only write custom wrappers for resources without standard wrappers.

Composing RAII Types (Rule of Zero)

The ultimate expression of RAII is the Rule of Zero: if all your member variables are RAII types, your class needs no user-declared special members. The compiler-generated destructor, copy, and move operations do the right thing automatically.

``cpp
class DatabaseSession {
std::unique_ptr conn_; // RAII: auto-closed
std::unique_lock lock_; // RAII: auto-unlocked
std::vector log_; // RAII: auto-freed
// No destructor, copy, or move needed — all member types handle it
};
``

The Rule of Zero is the goal. The Rule of Five (explicitly define all 5 special members) is for low-level resource wrapper types. There is no middle ground — if you declare any of the five, declare all of them.

Key Takeaways
  • RAII: acquire in constructor, release in destructor — automatic and exception-safe
  • Destructors are called during stack unwinding, so RAII handles exceptions naturally
  • RAII types should be move-only (exclusive ownership) or reference-counted (shared ownership)
  • Use std::scoped_lock (C++17) for deadlock-free multi-mutex locking
  • Unnamed RAII objects are destroyed immediately — always give them a name
  • Follow the Rule of Zero: use RAII members so the compiler generates correct special members

Quiz — Test Your Knowledge

(15 XP)

1. What is wrong with `std::lock_guard<std::mutex>(mtx);` (note: no variable name)?

2. Why should RAII types typically delete their copy operations?

3. What is the Rule of Zero?