Skip to content
C++

RAII

Resource Acquisition Is Initialization is the idiom that makes C++ resource management automatic, correct, and exception-safe — all at once. It is not a library feature; it is a design pattern baked into the language's object lifetime rules.

The problem: resources without ownership

Every resource in C++ — heap memory, a file handle, a mutex lock, a database connection, a socket — follows the same lifecycle: acquire it, use it, release it. In C, that lifecycle is managed by the programmer manually. The programmer must pair every fopen with an fclose, every malloc with a free, every mutex lock with an unlock.

That pairing is fragile. Real code has early returns, conditional branches, and — critically in C++ — exceptions. Any control-flow path that bypasses the release call produces a resource leak or a deadlock. The more complex the function, the more opportunities for a path to be missed.

// C-style manual management — every path must release
void process(const char* path) {
    FILE* f = fopen(path, "r");
    if (!f) return;           // ok: nothing to release

    if (!validate(path)) {
        fclose(f);            // must remember this
        return;
    }

    // ... more work that might throw in C++ ...

    fclose(f);                // easy to forget as the function grows
}

// With exceptions, it gets worse
void process_cpp(const std::string& path) {
    FILE* f = fopen(path.c_str(), "r");
    if (!f) throw std::runtime_error("open failed");

    do_work();   // throws? f leaks forever
    // do_more_work() can add yet another early-exit path

    fclose(f);   // only reached on the happy path
}

Herb Sutter and Andrei Alexandrescu put it bluntly in C++ Coding Standards: when you choose not to use RAII, you are "choosing the nontrivial and attention-intensive task of pairing the calls correctly by hand, including in the presence of branched control flows and exceptions." C++ gives you a better tool.

The core idea: tie resource lifetime to object lifetime

C++ guarantees that when an object with automatic storage duration (a local variable) goes out of scope, its destructor runs — regardless of how control left the scope. Early return, exception, or normal fall-through: the destructor always runs. This guarantee is the foundation of RAII.

The pattern is: acquire the resource in the constructor, release it in the destructor. Now the resource's lifetime is identical to the object's lifetime. As long as the object is alive, the resource is available. When the object dies — however that happens — the resource is released. You can no longer forget to release it because the language releases it for you.

// An RAII wrapper for FILE*
class FileHandle {
public:
    explicit FileHandle(const char* path, const char* mode) {
        f_ = fopen(path, mode);
        if (!f_) throw std::runtime_error("fopen failed");
        // Constructor: resource acquired, invariant established
    }

    ~FileHandle() {
        if (f_) fclose(f_);  // Destructor: resource released, always
    }

    FILE* get() const { return f_; }

    // Resources usually aren't copyable — disable copy
    FileHandle(const FileHandle&)            = delete;
    FileHandle& operator=(const FileHandle&) = delete;

private:
    FILE* f_;
};

// Now the function needs no manual cleanup at all
void process_cpp(const std::string& path) {
    FileHandle f(path.c_str(), "r");  // acquires on construction

    do_work(f.get());    // throws? destructor still runs → no leak
    do_more_work(f.get());

    // f destroyed here → fclose called automatically
}

The mental model: think of an RAII object as a scope guard. It stands at the door of a scope and commits to cleaning up when that scope closes, no matter the reason. The resource is a class invariant — it is available for the entire lifetime of the object.

Stack unwinding: why RAII and exceptions fit together

When an exception propagates, the C++ runtime performs stack unwinding: it destroys all fully-constructed local objects in each scope it exits, in reverse order of construction. This is not a special RAII feature — it is how destructors work. RAII just exploits that guarantee deliberately.

The cppreference documentation states the consequence precisely: "when exceptions occur, all resources acquired by every fully-constructed member and base subobject are released in reverse order of initialization." That means a function that acquires five RAII-wrapped resources and then throws releases all five, in reverse order — automatically, without any catch-and-cleanup code.

void multi_resource() {
    FileHandle    config("config.ini", "r");  // acquired 1st
    std::ofstream log("app.log");             // acquired 2nd
    std::lock_guard<std::mutex> lk(mtx_);    // acquired 3rd

    risky_work();   // throws std::runtime_error

    // Stack unwinds:
    // lk destroyed  → mutex unlocked      (3rd → 1st, reverse order)
    // log destroyed → ofstream flushed + closed
    // config destroyed → fclose called
    // No resource leaks, no deadlocks — zero cleanup code written
}

Standard library RAII wrappers

The C++ standard library is built on RAII. You rarely need to write raw resource management code because the standard already provides wrappers for the most common resources.

ResourceRAII wrapperAcquires / Releases
Heap memory (unique)std::unique_ptr<T>new / delete
Heap memory (shared)std::shared_ptr<T>new / delete (ref-counted)
Mutex lockstd::lock_guard<Mutex>mutex.lock() / mutex.unlock()
Flexible mutex lockstd::unique_lock<Mutex>lock() / unlock() (deferred, movable)
Filestd::fstream / std::ifstreamopen / close + flush
Threadstd::jthread (C++20)create / join
Dynamic arraystd::vector<T>allocator / deallocator
// std::lock_guard: the canonical RAII mutex wrapper
std::mutex mtx;

void update_shared_data() {
    std::lock_guard<std::mutex> lk(mtx);  // locks in constructor
    // ... modify shared data ...
    // lk destroyed here → unlocked automatically
    // Even if an exception propagates, the mutex is unlocked
}

// C++17: class template argument deduction — omit the template arg
std::lock_guard lk(mtx);  // deduced as std::lock_guard<std::mutex>

// std::unique_ptr: RAII for heap memory
auto buf = std::make_unique<char[]>(1024);  // allocates
process(buf.get());
// buf destroyed → delete[] called, no leak
// std::unique_ptr with custom deleter: RAII for any resource
// (not just heap memory)
struct FileDeleter {
    void operator()(FILE* f) const { if (f) fclose(f); }
};
using UniqueFile = std::unique_ptr<FILE, FileDeleter>;

UniqueFile open_file(const char* path) {
    FILE* f = fopen(path, "r");
    if (!f) throw std::runtime_error("fopen failed");
    return UniqueFile(f);  // wrapped immediately — never raw
}

void process() {
    auto f = open_file("data.bin");
    read_data(f.get());
    // f destroyed → fclose(f.get()) called
}

Exception safety guarantees

Exception safety is a contract a function makes about what state the program will be in if the function throws. There are three levels, formalized by David Abrahams and described in C++ Coding Standards. RAII is what makes it possible to achieve the stronger guarantees without heroic effort.

No-fail (nothrow)Required: destructors, deallocation, swap

The function never throws. The operation always succeeds. This is the strictest guarantee and must be provided by destructors, deallocation functions, and swap — because RAII and the strong guarantee depend on them being reliable.

StrongAim for: most mutating operations

The operation either fully succeeds (program reaches the target state) or fully rolls back to the state before the call — commit-or-rollback semantics. The program is left in the original valid state. Achieved via copy-then-swap and RAII.

BasicMinimum: all code must meet this

The operation may partially succeed, but the program is still in a valid state — invariants are preserved, no resources are leaked. A failure to meet even the basic guarantee is always a bug, not a design choice.

// Achieving the strong guarantee with copy-then-swap
// Pattern: do all work on a copy, then swap (swap must be no-fail)
void Widget::update(const Data& d) {
    Widget tmp(*this);          // copy current state
    tmp.apply(d);               // mutate the copy — may throw
    using std::swap;
    swap(*this, tmp);           // no-fail swap: commit the change
    // If apply() threw: *this is unchanged (strong guarantee)
    // If it succeeded: *this has the new state
}

// The swap must be no-fail — use std::swap or provide one:
void Widget::swap(Widget& other) noexcept {
    using std::swap;
    swap(data_, other.data_);
    swap(name_, other.name_);
}

Writing a correct RAII wrapper

Most resource wrappers follow a predictable structure. The critical decisions are: (1) should the wrapper be copyable? (usually not — resources aren't cloneable), (2) should it be movable? (usually yes — transfer ownership), and (3) what happens if acquisition fails in the constructor? (throw an exception — never construct a broken object).

// A movable, non-copyable RAII wrapper for a C-style handle
// Follows the same pattern as unique_ptr, but for arbitrary handles
template<typename Handle, typename Deleter>
class UniqueHandle {
public:
    explicit UniqueHandle(Handle h, Deleter d = Deleter{})
        : handle_(h), deleter_(std::move(d)) {}

    ~UniqueHandle() {
        if (handle_) deleter_(handle_);
    }

    // Move: transfer ownership
    UniqueHandle(UniqueHandle&& other) noexcept
        : handle_(std::exchange(other.handle_, Handle{}))
        , deleter_(std::move(other.deleter_)) {}

    UniqueHandle& operator=(UniqueHandle&& other) noexcept {
        if (this != &other) {
            if (handle_) deleter_(handle_);
            handle_ = std::exchange(other.handle_, Handle{});
            deleter_ = std::move(other.deleter_);
        }
        return *this;
    }

    // Copy: disabled — resources aren't copyable
    UniqueHandle(const UniqueHandle&)            = delete;
    UniqueHandle& operator=(const UniqueHandle&) = delete;

    Handle get() const { return handle_; }
    Handle release() { return std::exchange(handle_, Handle{}); }

private:
    Handle  handle_;
    Deleter deleter_;
};
// Usage: wrap any C-style resource with a matching deleter
struct CloseSocket { void operator()(int fd) { close(fd); } };

UniqueHandle<int, CloseSocket> sock(socket(AF_INET, SOCK_STREAM, 0));
connect(sock.get(), &addr, sizeof(addr));
// sock destroyed → close(fd) called automatically

Scope guards: ad-hoc RAII for one-off cleanup

Sometimes you need cleanup at scope exit but don't want to write a full wrapper class. A scope guard captures a callable and invokes it on destruction. C++23 standardizes this as std::scope_exit (from <scope>). For older standards, a minimal implementation is straightforward.

// C++23: std::scope_exit
#include <scope>   // C++23

void configure_system() {
    enable_debug_mode();
    auto cleanup = std::scope_exit([] { disable_debug_mode(); });

    do_setup();    // throws? cleanup still runs
    // cleanup invokes lambda on scope exit
}

// Pre-C++23: minimal scope_guard (used by many codebases)
template<typename F>
class ScopeGuard {
public:
    explicit ScopeGuard(F f) : f_(std::move(f)) {}
    ~ScopeGuard() { f_(); }
    ScopeGuard(const ScopeGuard&) = delete;
    ScopeGuard& operator=(const ScopeGuard&) = delete;
private:
    F f_;
};

// Deduction guide (C++17) or helper function (C++14)
template<typename F>
ScopeGuard<F> make_scope_guard(F f) { return ScopeGuard<F>(std::move(f)); }

void setup_context(Context* ctx) {
    ctx->begin();
    auto guard = make_scope_guard([ctx] { ctx->end(); });
    // ... work that might throw ...
}

Prefer named RAII wrappers over scope guards. A scope guard with an inline lambda is fine for one-off glue code, but if the same cleanup pattern appears more than once, encapsulate it in a proper RAII class. Named types are easier to test, to reason about, and to reuse.

Copy, move, and RAII: the Rule of Five connection

Every RAII class manages a resource, which means the compiler-generated copy operations are almost certainly wrong. A shallow copy of a file handle would give you two objects claiming to own the same handle — and the first to be destroyed would close it, leaving the other with a dangling resource.

The Rule of Five applies directly: if you define a destructor (which every RAII class does), you must also think carefully about the copy constructor, copy assignment operator, move constructor, and move assignment operator. Usually the answer is: delete copy, implement move (transfer ownership), or use = defaultonce you've verified the compiler-generated version is correct (e.g., when all members are already movable RAII wrappers themselves).

class Connection {
public:
    explicit Connection(const std::string& url);
    ~Connection() { if (handle_) disconnect(handle_); }

    // Move-only: ownership transfers, no dual ownership
    Connection(Connection&& o) noexcept
        : handle_(std::exchange(o.handle_, nullptr)) {}

    Connection& operator=(Connection&& o) noexcept {
        if (this != &o) {
            if (handle_) disconnect(handle_);
            handle_ = std::exchange(o.handle_, nullptr);
        }
        return *this;
    }

    Connection(const Connection&)            = delete;  // no copy
    Connection& operator=(const Connection&) = delete;

private:
    Handle* handle_ = nullptr;
};

// If all members are themselves RAII types, = default often works:
class Session {
    Connection     conn_;   // already RAII + move-only
    std::vector<std::string> log_;  // also RAII
public:
    Session(Session&&) noexcept = default;             // correct by default
    Session& operator=(Session&&) noexcept = default;  // correct by default
    // No explicit destructor needed — members clean themselves up
};

Key rules for RAII

Acquire in the constructor, release in the destructor

Never split resource acquisition from the owning object's construction.

Never allocate more than one resource in a single statement

Fun(new A, new B) can leak if one allocation succeeds and the other throws before the wrapper is constructed. Always: auto a = make_unique<A>(); Fun(std::move(a), make_unique<B>()).

Immediately give raw resources to an owning object

The moment you call fopen() / malloc() / socket(), wrap it. Don't hold a raw resource naked, even temporarily.

Destructors must never throw

A destructor invoked during stack unwinding that throws terminates the program. All standard library destructors are noexcept; yours should be too.

Delete or implement copy; don't ignore it

Compiler-generated copy of a resource wrapper is almost always wrong. Make an active decision: delete copy (move-only), or deep-copy the resource (rare).

Sign in to track progress