Skip to content
C++
Idiom
since C++98
Basic

RAII

Resource Acquisition Is Initialization — tie resource lifetime to object lifetime for deterministic, exception-safe cleanup with zero overhead.

RAIIsince C++98

Resource Acquisition Is Initialization: bind the lifetime of any resource to the lifetime of a stack object so that the language's guaranteed destructor semantics handle release on every exit path, including exception propagation.

Overview

RAII is not a library feature — it is a design discipline that leverages two guarantees C++ makes unconditionally:

  1. When an automatic (stack) object goes out of scope, its destructor runs.
  2. During stack unwinding caused by an exception, destructors of all fully-constructed local objects still run.

Together these mean: if you acquire a resource inside a constructor and release it inside the corresponding destructor, the release is automatic on every exit path — normal return, early return, and exception. No try/finally, no goto cleanup, no __attribute__((cleanup)).

The idiom predates C++98 but was named and popularised in that era. Every standard since has added library types that encode it: std::auto_ptr (deprecated in C++11, removed in C++17), std::unique_ptr and std::shared_ptr (C++11), std::lock_guard (C++11), std::scoped_lock (C++17), and std::scope_exit/std::scope_fail/std::scope_success (C++23, <scope>).

Syntax

An RAII class has a canonical shape: constructor acquires, destructor releases, copy is either deleted or well-defined, move transfers ownership.

cpp
class Handle {
    Resource* res;
public:
    explicit Handle(ResourceConfig cfg)        // acquire in ctor
        : res(Resource_create(cfg)) {
        if (!res) throw std::runtime_error("Resource_create failed");
    }

    ~Handle() noexcept { Resource_destroy(res); }  // release in dtor

    // Non-copyable — resource has unique ownership
    Handle(const Handle&)            = delete;
    Handle& operator=(const Handle&) = delete;

    // Movable — transfer ownership  (C++11)
    Handle(Handle&& o) noexcept : res(o.res) { o.res = nullptr; }
    Handle& operator=(Handle&& o) noexcept {
        if (this != &o) {
            Resource_destroy(res);
            res   = o.res;
            o.res = nullptr;
        }
        return *this;
    }

    Resource* get() const noexcept { return res; }
};

The move constructor and move assignment were introduced in C++11. Prior to that, RAII wrappers were often non-copyable and non-movable, limiting composability.

Examples

File handle

std::fstream already applies RAII, but a C-API wrapper shows the pattern clearly:

cpp
#include <cstdio>
#include <stdexcept>

class CFile {
    FILE* fp_ = nullptr;
public:
    explicit CFile(const char* path, const char* mode)
        : fp_(std::fopen(path, mode)) {
        if (!fp_) throw std::system_error(errno, std::generic_category(), path);
    }

    ~CFile() noexcept {
        if (fp_) std::fclose(fp_);   // fclose never throws
    }

    CFile(const CFile&)            = delete;
    CFile& operator=(const CFile&) = delete;

    CFile(CFile&& o) noexcept            : fp_(o.fp_) { o.fp_ = nullptr; }
    CFile& operator=(CFile&& o) noexcept {
        if (fp_) std::fclose(fp_);
        fp_   = o.fp_;
        o.fp_ = nullptr;
        return *this;
    }

    FILE* get() const noexcept { return fp_; }
};

void appendLog(const char* msg) {
    CFile f("app.log", "a");
    std::fputs(msg, f.get());
    std::fputc('\n', f.get());
}   // fclose happens here — always

Mutex lock guard

cpp
#include <mutex>

std::mutex g_mtx;

void transfer(int& src, int& dst, int amount) {
    std::lock_guard<std::mutex> lk(g_mtx);  // locks in ctor  (C++11)
    src -= amount;
    dst += amount;
}   // lk destructor unlocks — even if an exception propagates

// C++17: lock multiple mutexes deadlock-free with scoped_lock
std::mutex mtx_a, mtx_b;
void swap_balances(int& a, int& b) {
    std::scoped_lock lk(mtx_a, mtx_b);   // C++17 — CTAD deduces types
    std::swap(a, b);
}

Scope guard — ad-hoc cleanup without a full class

When the resource is not reusable enough to justify a dedicated wrapper, a scope guard handles one-off rollback logic:

cpp
#include <functional>
#include <utility>

class ScopeGuard {
    std::function<void()> fn_;
    bool active_ = true;
public:
    explicit ScopeGuard(std::function<void()> f) : fn_(std::move(f)) {}
    ~ScopeGuard() noexcept { if (active_) fn_(); }

    void release() noexcept { active_ = false; }

    ScopeGuard(const ScopeGuard&)            = delete;
    ScopeGuard& operator=(const ScopeGuard&) = delete;
};

void patchDatabase(Db& db) {
    db.beginTransaction();
    ScopeGuard rollbackGuard([&]{ db.rollback(); });

    db.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
    db.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2");

    rollbackGuard.release();  // all succeeded — don't rollback
    db.commit();
}

C++23 standardised three variants in <scope>:

cpp
#include <scope>  // C++23

void patchDatabase(Db& db) {
    db.beginTransaction();
    auto guard = std::scope_fail([&]{ db.rollback(); });  // C++23 — runs only on exception

    db.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
    db.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2");

    db.commit();
    // guard destructor: no exception in flight, so rollback is NOT called
}

std::scope_exit runs unconditionally; std::scope_fail runs only when an exception is propagating; std::scope_success runs only when no exception propagates.

Rule of Zero in practice

The ideal is to compose your types entirely from existing RAII types so the compiler generates all five special members correctly without any hand-written code:

cpp
#include <memory>
#include <string>
#include <vector>

class Session {
    std::unique_ptr<Socket>   socket_;   // C++11 — unique ownership, non-copyable
    std::vector<std::string>  log_;      // value semantics, freely copyable/movable
    std::string               user_id_;  // value semantics

    // No destructor declared. No copy/move declared.
    // Compiler-generated dtor destroys socket_, log_, user_id_ in reverse order.
    // Compiler-generated move ctor/assignment moves each member.
    // Copy ctor/assignment are implicitly deleted (because unique_ptr is non-copyable).
};

This is the Rule of Zero (a C++11-era guideline): if you can express ownership through composing existing RAII wrappers, write zero special member functions. Reserve the Rule of Five for low-level resource wrappers that sit at the bottom of the ownership hierarchy.

Best Practices

Acquire in the constructor body, not in a separate init() method. Two-phase initialisation defeats RAII: if init() isn't called, the object is half-formed, and there is no destructor checkpoint that knows whether acquisition succeeded.

Destructors must not throw. Since C++11, destructors are implicitly noexcept. If a destructor throws during stack unwinding, std::terminate is called. Catch all exceptions inside the destructor and log or swallow them.

Null-check before release in move semantics. After a move, the source object must be in a valid destructible state. The canonical idiom is to set the moved-from pointer to nullptr and guard the destructor's release call with a null check.

Prefer std::unique_ptr over hand-rolled wrappers for heap memory. Hand-rolling is appropriate for non-memory resources (file descriptors, sockets, GPU handles). For heap, std::unique_ptr and std::shared_ptr (C++11) are battle-tested and compose cleanly with std::make_unique (C++14) and std::make_shared (C++11).

Use std::scoped_lock over std::lock_guard when locking multiple mutexes. std::scoped_lock (C++17) applies a deadlock-avoidance algorithm; std::lock_guard (C++11) does not.

Common Pitfalls

Destructor that throws

cpp
class Bad {
    ~Bad() {
        close();    // close() may throw — undefined behaviour during unwinding
    }
};

class Good {
    ~Good() noexcept {
        try { close(); }
        catch (const std::exception& e) { logError(e.what()); }
        catch (...) {}
    }
};

Copying an owning wrapper without a custom copy constructor

cpp
CFile a("data.bin", "rb");
CFile b = a;    // deleted — compile error (correct)

// Without `= delete`, the compiler-generated copy would copy the pointer.
// Both a and b would try to fclose the same FILE* on destruction — double-free.

Storing RAII objects in raw containers without move awareness

Before C++11, inserting an RAII object into std::vector required copying, which either failed (if copy was deleted) or double-released. Since C++11, std::vector will move elements when it reallocates, provided the move constructor is noexcept. Mark your move constructors noexcept to enable this optimisation.

cpp
std::vector<CFile> files;
files.push_back(CFile("a.log", "a"));  // move-constructs into vector  (C++11)
files.emplace_back("b.log", "a");      // constructs in-place          (C++11)
// CFile's move ctor is noexcept — vector reallocation moves, not copies

Relying on RAII for resources that outlive their thread

RAII ties lifetime to scope. If a resource must outlive its creator (e.g., a background thread holds a reference), scope-based cleanup fires too early. Use std::shared_ptr or explicit lifetime management to extend resource life beyond a single stack frame.

std::scope_exit, std::scope_fail, std::scope_success (C++23)

C++23 formalises the scope-guard pattern with three vocabulary types in <scope>. std::scope_exit always executes its callable when the enclosing scope exits; std::scope_fail executes only when the scope exits via an exception; std::scope_success executes only when no exception propagates. All three use std::uncaught_exceptions() internally to distinguish the success and failure paths.

cpp
#include <scope>  // C++23

void process(Db& db) {
    db.beginTransaction();

    // Runs unconditionally — always rolls back or logs on exit
    std::scope_exit cleanup([&]{ db.disconnect(); });  // C++23

    // Runs only when an exception propagates out of this scope
    std::scope_fail  on_fail([&]{ db.rollback(); });   // C++23

    // Runs only when scope exits normally (no exception)
    std::scope_success on_ok([&]{ db.commit(); });     // C++23

    db.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
    db.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
}   // on exception: on_fail + cleanup run; on success: on_ok + cleanup run

Each guard exposes a .release() member to disarm it — the callable is then never invoked. This mirrors the manual ScopeGuard::release() shown earlier, but is now a standard vocabulary.

cpp
#include <scope>  // C++23

void conditionalCleanup(Resource& r) {
    std::scope_exit guard([&]{ r.revert(); });  // C++23

    if (r.tryApply()) {
        guard.release();  // success — disarm the rollback
    }
    // If tryApply() threw or returned false, revert() is called on scope exit
}

Contrast with a hand-rolled ScopeGuard: the standard versions are non-copyable, correctly handle exceptions thrown by the callable itself (they catch and terminate rather than letting a second exception propagate), and interact safely with std::uncaught_exceptions() even across nested scopes.

RAII for Network and OS Resources

Any resource identified by an integer handle or opaque pointer is a candidate for RAII wrapping. POSIX file descriptors, sockets, and memory-mapped regions are the most common examples where std::fstream and std::unique_ptr do not apply directly.

cpp
#include <unistd.h>   // close
#include <sys/mman.h> // mmap, munmap
#include <stdexcept>

// Generic file-descriptor guard                         // C++11
class FileDescriptorGuard {
    int fd_;
public:
    explicit FileDescriptorGuard(int fd) : fd_(fd) {
        if (fd_ < 0) throw std::runtime_error("invalid fd");
    }
    ~FileDescriptorGuard() noexcept { ::close(fd_); }

    FileDescriptorGuard(const FileDescriptorGuard&)            = delete;
    FileDescriptorGuard& operator=(const FileDescriptorGuard&) = delete;
    FileDescriptorGuard(FileDescriptorGuard&& o) noexcept : fd_(o.fd_) { o.fd_ = -1; }
    FileDescriptorGuard& operator=(FileDescriptorGuard&& o) noexcept {
        if (this != &o) { ::close(fd_); fd_ = o.fd_; o.fd_ = -1; }
        return *this;
    }

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

// mmap guard — calls munmap on destruction                // C++11
class MmapGuard {
    void*  addr_;
    size_t length_;
public:
    MmapGuard(void* addr, size_t len) : addr_(addr), length_(len) {
        if (addr_ == MAP_FAILED) throw std::runtime_error("mmap failed");
    }
    ~MmapGuard() noexcept { ::munmap(addr_, length_); }

    MmapGuard(const MmapGuard&)            = delete;
    MmapGuard& operator=(const MmapGuard&) = delete;

    void* get() const noexcept { return addr_; }
};

For simple cases where a destructor is a single function call, std::unique_ptr with a custom deleter is a one-liner alternative:

cpp
#include <memory>    // C++11
#include <unistd.h>

// unique_ptr wrapping a raw fd — deleter calls close()
auto fd = std::unique_ptr<int, decltype(&::close)>(
    new int(::open("data.bin", O_RDONLY)),
    &::close
);
// fd goes out of scope → ::close(*fd) is called automatically

The custom-deleter form is concise but awkward for non-pointer integer handles; a dedicated wrapper class is cleaner when the resource is used extensively throughout a codebase.

RAII in Multithreaded Code

Bare mutex.lock() / mutex.unlock() calls are unsafe under exceptions: if code between them throws, unlock() is never reached and the mutex stays locked forever. RAII lock types eliminate this hazard entirely.

cpp
#include <mutex>
#include <shared_mutex>

std::mutex g_mtx;

// Simple exclusive lock — ctor locks, dtor unlocks           // C++11
void increment(int& counter) {
    std::lock_guard<std::mutex> lk(g_mtx);
    ++counter;
}   // lk destructor unlocks even if ++ throws

// Deadlock-free acquisition of multiple mutexes              // C++17
std::mutex mtx_a, mtx_b;
void swap_accounts(int& a, int& b) {
    std::scoped_lock lk(mtx_a, mtx_b);  // C++17 — CTAD, uses std::lock internally
    std::swap(a, b);
}

// Deferred or conditional locking                            // C++11
void maybeTransfer(std::mutex& m, int& src, int& dst, int amount) {
    std::unique_lock<std::mutex> lk(m, std::defer_lock);  // C++11 — not yet locked
    if (src >= amount) {
        lk.lock();
        src -= amount;
        dst += amount;
    }
}

// Shared (reader-writer) lock                                // C++17
std::shared_mutex rw_mtx;
int sharedData = 0;

void reader() {
    std::shared_lock lk(rw_mtx);  // C++17 — multiple readers allowed concurrently
    (void)sharedData;
}

void writer() {
    std::unique_lock lk(rw_mtx);  // C++17 — exclusive write access
    ++sharedData;
}

std::scoped_lock (C++17) supersedes std::lock_guard for new code: it handles the single-mutex case identically and adds deadlock-safe multi-mutex locking. std::unique_lock is the right choice when you need to unlock early, use condition variables, or defer the initial lock.

RAII Wrappers for Third-Party C APIs

C libraries expose resources as opaque pointers paired with a create/destroy function — the standard idiom is std::unique_ptr with a custom deleter. This gives you RAII, move semantics, and zero overhead with minimal boilerplate.

cpp
#include <memory>
#include <sqlite3.h>   // C++11

// Custom deleter for sqlite3*
struct Sqlite3Closer {
    void operator()(sqlite3* db) const noexcept { sqlite3_close(db); }
};
using UniqueDb = std::unique_ptr<sqlite3, Sqlite3Closer>;

// Factory helper — mirrors std::make_unique                  // C++11
UniqueDb openDatabase(const char* path) {
    sqlite3* raw = nullptr;
    if (sqlite3_open(path, &raw) != SQLITE_OK) {
        sqlite3_close(raw);
        throw std::runtime_error(sqlite3_errmsg(raw));
    }
    return UniqueDb(raw);
}

void query(const char* path) {
    auto db = openDatabase(path);   // C++11
    // ... use db.get() ...
}   // sqlite3_close called automatically

The same pattern applies to SDL surfaces and any other C library handle:

cpp
#include <SDL2/SDL.h>  // C++11

// decltype(&SDL_FreeSurface) deduces the deleter type automatically
auto surf = std::unique_ptr<SDL_Surface, decltype(&SDL_FreeSurface)>(
    SDL_LoadBMP("sprite.bmp"),
    &SDL_FreeSurface
);
// SDL_FreeSurface(surf.get()) called on destruction

When the library type is forward-declared (opaque handle pattern), the deleter must be defined in a translation unit where the complete type is visible — place the destructor of any wrapper class in a .cpp file, not a header, to avoid incomplete-type errors.

RAII and Move Semantics: Transferring Ownership

An RAII wrapper should be movable but not copyable: copying would duplicate the resource handle and cause a double-release on destruction. This is precisely the contract of std::unique_ptr, which is the canonical RAII wrapper for heap memory.

cpp
#include <memory>  // C++11

class SocketGuard {
    int fd_ = -1;
public:
    explicit SocketGuard(int fd) : fd_(fd) {}
    ~SocketGuard() noexcept { if (fd_ >= 0) ::close(fd_); }

    // Non-copyable                                           // C++11
    SocketGuard(const SocketGuard&)            = delete;
    SocketGuard& operator=(const SocketGuard&) = delete;

    // Movable — transfers ownership                         // C++11
    SocketGuard(SocketGuard&& o) noexcept : fd_(o.fd_) { o.fd_ = -1; }
    SocketGuard& operator=(SocketGuard&& o) noexcept {
        if (this != &o) { if (fd_ >= 0) ::close(fd_); fd_ = o.fd_; o.fd_ = -1; }
        return *this;
    }

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

This is the Rule of Five applied to a resource wrapper. For most application-level types, the Rule of Zero is preferable: compose from existing RAII types so the compiler generates all special members correctly.

cpp
// Rule of Zero: no special members needed                    // C++11
class Connection {
    std::unique_ptr<SocketGuard> sock_;  // move-only because unique_ptr is move-only
    std::string                  host_;  // freely movable and copyable
    // Compiler-generated dtor, move ctor, move assignment are all correct.
    // Copy ctor and copy assignment are implicitly deleted.
};

Write a custom RAII class (Rule of Five) only when the resource is not a pointer — integer file descriptors, GPU command queues, OS handles — or when the cleanup logic is non-trivial. For heap memory, always prefer std::unique_ptr or std::shared_ptr over a hand-rolled wrapper.

RAII vs Finally: Design Philosophy

RAII and finally blocks solve the same problem — guaranteed cleanup — but from opposite directions. finally is statement-based: the programmer writes cleanup code at the call site as a separate block. RAII is ownership-based: cleanup logic lives in the destructor of the owning type and is reused everywhere that type is instantiated.

cpp
// RAII — cleanup is part of the type, reused everywhere     // C++11
{
    std::unique_ptr<Resource> r = makeResource();
    useResource(*r);
}   // destructor runs here — always, even on exception

// std::scope_exit approximates finally for ad-hoc cases     // C++23
#include <scope>
{
    Resource* r = acquireResource();
    std::scope_exit finally_block([&]{ releaseResource(r); });  // C++23
    useResource(*r);
}   // releaseResource called unconditionally

C++ chose RAII over finally for a fundamental reason: destructors compose. A finally block in another language does not run if the language runtime crashes or if a second exception is thrown during the first cleanup. C++ destructor ordering is defined by the standard and executes during stack unwinding in a well-specified reverse-construction order.

MechanismScopeLanguageReusableException-safeZero overhead
RAII classType lifetimeC++Yes — across all call sitesYesYes
std::scope_exitSingle scopeC++23No — per call siteYesNear-zero
finally blockSingle scopeGo / Java / PythonNo — per call sitePartialNo

std::scope_exit (C++23) is the right tool for ad-hoc cleanup that does not justify a dedicated wrapper class. For any resource used in more than one place, a proper RAII class is always the superior choice: it names the ownership contract, documents the cleanup invariant, and eliminates repeated boilerplate.

See Also

  • std::unique_ptr — the canonical RAII wrapper for exclusive heap ownership (C++11)
  • std::shared_ptr — reference-counted RAII for shared ownership (C++11)
  • std::lock_guard, std::unique_lock, std::scoped_lock — mutex RAII (C++11 / C++17)
  • std::scope_exit, std::scope_fail, std::scope_success — generic scope guards (C++23, <scope>)
  • Move semantics — prerequisite for movable RAII wrappers (C++11)
  • Rule of Five / Rule of Zero — guidelines for when to write or omit special member functions