본문으로 건너뛰기
Previous
Next
C++ RAII Pattern Complete Guide

C++ RAII Pattern Complete Guide

C++ RAII Pattern Complete Guide

이 글의 핵심

Master C++ RAII: automatic resource cleanup through constructor/destructor pairing. Complete guide with smart pointers, locks, and production patterns.

Why RAII

I learned about RAII the hard way: a database connection leak in production. The service looked fine under light load, but as traffic grew, the process slowly exhausted the connection pool. We closed the client on the happy path and on most error branches, but one code path could throw after the handle was opened. The stack unwound, our manual close() did not run on that path, and the ORM- or driver-level connection stayed live until the next deploy. Log graphs looked like a slow memory climb; the root cause was not heap fragmentation but connections that never rejoined the pool. That week is when “acquire in the constructor, release in the destructor” stopped sounding like a textbook phrase and started sounding like a rule you follow so you can sleep.

C++ has no finally block. When control leaves a scope by exception or return, destructors of fully constructed automatic objects run in reverse order as the stack unwinds. If resources are not owned by those objects, you end up duplicating cleanup after every return, hoping every new contributor remembers every branch. In reviews, the bugs rarely sit on the first page of a function; they sit in the early exit nobody runs in the debugger.

Single cleanup path beats five copies of fclose or connection->close(). Unwind-safe behavior means a thrown exception in the middle of a function does not leak the half-opened resource. Clear ownership removes the debate about which pointer “owns” heap memory or a handle. And composability matters: a file can live inside a transaction inside a lock, each wrapper responsible for its own teardown, without turning the function into a maze of goto cleanup labels.

A raw pointer in application code is often just observation; ownership belongs in smart pointers, containers, and RAII handles (R.3, I.11).


What is RAII

RAII (Resource Acquisition Is Initialization) is the C++ idiom of binding a resource’s lifetime to the lifetime of a stack-allocated (or member) object. The name encodes a strict rule: a resource is considered owned once an object is fully constructed with that resource acquired, and it is released in the object’s destructor when the object is destroyed. This is not a library feature; it is a contract you express through class design, and it is one of the mechanisms that make C++ suitable for deterministic, low-overhead resource management in systems and application code.

The C++ Core Guidelines frame this in practice as “use RAII, never use naked new/delete in application code” (see, for example, F.1–F.6, R.1–R.5), and to prefer types whose destructors are noexcept for resource release paths (C.37). The goal is to turn invariants and cleanup into type-enforced behavior: if an object exists, the resource is valid; if the object goes out of scope—normally or during stack unwinding—cleanup runs exactly once, in a known order (reverse order of construction for automatic objects in the same scope).

This article connects RAII to smart pointers, mutex and lock types, file streams, transactional patterns, and scope guards (defer / finally-like behavior). It also addresses move-only ownership, exception-safety levels, and how RAII composes in larger programs. For broader pattern context, see the design patterns overview and the dedicated RAII deep dive.

// Core idea: a named owner whose destructor performs release.
#include <cstdio>
#include <stdexcept>
#include <memory>

struct File {
    std::FILE* f_{};
    explicit File(const char* path, const char* mode) {
        f_ = std::fopen(path, mode);
        if (!f_) throw std::runtime_error("open failed");
    }
    ~File() { if (f_) std::fclose(f_); } // noexcept: resource release
    std::FILE* get() const noexcept { return f_; }
    File(const File&) = delete;
    File& operator=(const File&) = delete;
};

Core principles: constructor acquires, destructor releases

  1. Acquire a resource in the constructor (or in a static factory) such that, on success, the object is ready to use.
  2. Release the resource in the destructor. For resource-release APIs that cannot throw, keep the destructor noexcept; if a release might need to report failure, expose an explicit close() and document whether the destructor performs a “best effort” silent release.
  3. One owner of an exclusive resource at a time, unless the resource is designed for shared ownership (e.g. std::shared_ptr).

When multiple members own resources, member destruction order is the reverse of declaration order. You lean on that ordering: declare members so that a dependency is destroyed before the object it needs for safe teardown—though ideally each member is self-contained.

Partial construction: If a constructor throws after some members were constructed, only those subobjects that completed construction will be destroyed, in reverse order. That is why wrapping raw new or OS handles in std::unique_ptr before the next step that can throw is critical: a thrown exception will still run the already-constructed unique_ptr’s destructor, avoiding leaks.

Core Guidelines cross-reference: C.32 (define a default destructor with = default if members are RAII and need no custom logic) and C.30 (destructors, swap, memory deallocation, and swap for types used with standard containers should be noexcept).


Basic examples: files, mutexes, memory

File handles and C stream APIs

The standard library’s high-level I/O is usually std::fstream, but C APIs (FILE*, POSIX fd, Win32 HANDLE) remain common. Wrap them: std::unique_ptr<FILE, custom_deleter> or a small File class, as in the example under What is RAII above.

#include <cstdio>
#include <memory>

struct Fclose {
    void operator()(std::FILE* f) const noexcept { if (f) std::fclose(f); }
};

std::unique_ptr<std::FILE, Fclose> open_file(const char* path) {
    auto* f = std::fopen(path, "r");
    if (!f) throw std::runtime_error("open");
    return {f};
}

Mutex: never pair raw lock with manual unlock

The fix is a lock guard (covered in depth below): always pair mutex acquisition with an RAII lock object on the stack in the same scope that performs the work.

Heap memory

Prefer:

auto p = std::make_unique<Widget>(args...);

over new + raw pointer lifetimes, except in low-level code where you are implementing an owner type. make_unique and make_shared are exception-safe in the face of other subexpressions that might throw (R.11, R.14).


Smart pointers: unique_ptr, shared_ptr, weak_ptr as RAII

std::unique_ptr<T, D> is exclusive ownership: you cannot copy it, but you can move it. When the last unique_ptr for that allocation dies, the custom deleter D runs once. std::shared_ptr<T> implements shared ownership with a reference count; you can copy (which bumps the count) and move. Release happens when the count hits zero, which disposes of the T and the control block. std::weak_ptr<T> does not own T—it only observes. You can copy and move weak pointers freely; they do not participate in the refcount that keeps T alive, and lock() may return an empty shared_ptr if the object is already gone.

  • unique_ptr: default choice for heap single ownership. Often zero overhead over a raw pointer, with a known deallocation path. Move-only, which matches most exclusive OS handles and GPU resources.
  • shared_ptr: use when true shared ownership is the model (graphs, async shares). Remember atomic refcount cost and separate control block allocation (unless you use make_shared to merge block with T when appropriate).
  • weak_ptr: break cycles, caches, and “maybe still alive” observers. Never assumes the object is live until lock() succeeds.

weak_ptr is still RAII for the control block link; it just does not own T’s storage.

Core Guidelines: R.1 (high-level: manage resources with RAII), R.3 (raw T* is non-owning), R.5 (minimize shared_ptr unless shared ownership is the design), I.5 (assume RAII invariants hold).

#include <memory>
#include <iostream>

struct Node {
    int v{};
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev; // break cycles: observer only
};

void use_chain() {
    auto a = std::make_shared<Node>();
    auto b = std::make_shared<Node>();
    a->next = b;
    b->prev = a; // weak: no refcount bump on a back-edge
    if (auto p = b->prev.lock()) {
        std::cout << p->v; // a still alive
    }
}

Lock guards: std::lock_guard, std::unique_lock, std::scoped_lock

std::lock_guard

Minimal wrapper: locks in constructor, unlocks in destructor. No manual unlock on normal paths. Use when you need a fixed, simple critical section and no condition variable waiting (which needs unique_lock).

#include <mutex>

std::mutex m;

void f() {
    std::lock_guard<std::mutex> lock(m);
    // critical section: data protected by m
} // unlock here

std::unique_lock

Deferred locking, try_lock, timed lock, and work with std::condition_variable (which requires unique_lock for wait). Still RAII, but more control than lock_guard.

std::mutex m2;
std::condition_variable cv;
bool ready = false;

void wait_example() {
    std::unique_lock<std::mutex> lock(m2);
    cv.wait(lock, [] { return ready; });
} // unique_lock may unlock in destructor

std::scoped_lock (C++17)

Takes multiple mutexes and locks them without deadlock in a single call (adheres to a consistent lock order internally). Prefer for multi-mutex scope when you need to hold more than one lock at a time.

#include <mutex>

std::mutex a, b;

void g() {
    std::scoped_lock lock(a, b); // deadlock-avoiding multi-lock
} // both released

Pitfall: Do not call mutex member functions directly if a guard is already responsible for the same mutex in the same scope. Double-lock (same thread, non-recursive mutex) is undefined behavior in typical std::mutex usage.


Custom RAII classes: writing your own wrappers

Guidelines for handle wrappers:

  1. Validate the acquisition; throw on failure so the object never represents an invalid owned state (or use std::optional<Handle> / std::expected in C++23 for nullable success).
  2. Delete copy operations or implement Rule of Five with clear ownership (often move-only is best for sole ownership of a handle).
  3. noexcept destructor when your release is not allowed to throw (typical for close/fclose/free in destructors: swallow errors or log, C.37).
  4. If you need release() (transfer ownership to C API that takes over), return the raw handle and set internal state to empty to avoid double free.
#include <cstddef>

// Example: move-only, exclusive ownership
class Buffer {
    unsigned char* p_{};
    std::size_t n_{};

public:
    explicit Buffer(std::size_t n) : p_(new unsigned char[n]()), n_(n) {}
    ~Buffer() noexcept { delete[] p_; }

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

    Buffer(Buffer&& o) noexcept : p_(o.p_), n_(o.n_) { o.p_ = nullptr; o.n_ = 0; }
    Buffer& operator=(Buffer&& o) noexcept {
        if (this != &o) {
            delete[] p_;
            p_ = o.p_; n_ = o.n_;
            o.p_ = nullptr; o.n_ = 0;
        }
        return *this;
    }
    unsigned char* data() noexcept { return p_; }
    std::size_t size() const noexcept { return n_; }
};

Move semantics: RAII and move-only types

Exclusive resource ownership is naturally move-only: transferring ownership is std::move, not copy. The moved-from object must remain in a valid (often empty) state; its destructor must still be safe (often a no-op release when null).

  • = default move is appropriate when your members (e.g. unique_ptr, string, vector) already have correct move semantics—Rule of Zero (C.20–C.22).
  • noexcept move matters for std::vector reallocation: if move might throw, vector may use copy to preserve the strong exception guarantee, which can be costly for large T (C.17: move should be noexcept for types stored in vector in performance-sensitive code).
#include <memory>
struct Heavy {
    std::unique_ptr<int[]> data;
    Heavy() : data(std::make_unique<int[]>(1'000'000)) {}
    // Move generated or defaulted; unique_ptr is noexcept move
};

Exception safety: strong guarantee with RAII

Exception safety is described in terms of invariants after an operation that throws:

  • Nothrow guarantee: operation does not throw.
  • Strong guarantee: if an exception propagates, program state is as if the operation never started (often via copy-and-swap or all-or-nothing side effects in a temporary).
  • Basic guarantee: invariants of objects hold; no leaks, no UB, but observable state can be a valid intermediate.
  • No guarantee: (avoid in library code) undefined or broken invariants.

RAII provides leak-free basic guarantee for resources owned by well-formed objects. Strong guarantee often combines RAII (no leaks) with transaction-style or copy-then-commit logic.

Copy-and-swap sketch (Core Guidelines C.80 pattern spirit):

#include <vector>
#include <utility>

class Buffer2 {
    std::vector<int> v_;
public:
    void replace_with(const std::vector<int>& other) {
        std::vector<int> temp = other; // if this throws, v_ unchanged
        v_.swap(temp);                 // nothrow swap of vectors
    }
};

Never throw from a destructor: during stack unwinding, a second throw leads to std::terminate. If release must surface errors, use ~T() noexcept + explicit bool close() noexcept and document behavior (C.37).


RAII for transactions: database, network, and file transactions

The pattern: on success commit, on scope exit without commit, rollback in the destructor. Guard against double commit and re-entry.

struct Database { // example stub
    void begin() {}
    void commit() {}
    void rollback() {}
    void run(const char*) {}
};

struct Transaction {
    Database& db_;
    bool done_{false};

    explicit Transaction(Database& db) : db_(db) { db_.begin(); }
    void commit() {
        if (!done_) { db_.commit(); done_ = true; }
    }
    ~Transaction() {
        if (!done_) db_.rollback();
    }
    Transaction(const Transaction&) = delete;
    Transaction& operator=(const Transaction&) = delete;
};

void work(Database& db) {
    Transaction t(db);
    db.run("UPDATE ...");
    t.commit();
}

File transactions at the app level: write to a temporary file, fsync, then rename atomically (POSIX) when committed; destructor removes the temp file on failure. That is still RAII for the temp path’s std::ofstream or the temp directory object.

Network “transaction” is often a state machine: RAII for socket lifetime and a sending guard that flushes; application-level idempotency is separate from C++ object lifetime but benefits from the same scoping discipline.


Scope guards: defer patterns and finally-like behavior

A scope guard is a small RAII object that runs a callback (often a lambda) on scope exit, success or failure—like Go’s defer in spirit. C++20 does not standardize a single scope_exit in the core (libraries like GSL and Boost have variants; C++ proposals exist). For portability, a short template in your detail namespace is common.

Properties:

  • Move-only; dismiss to cancel the callback on success.
  • Destructor should usually be noexcept; if the callback might throw, wrap in try/catch and log, or use policy to decide terminate vs swallow.
#include <utility>
#include <type_traits>

template <typename F>
class ScopeGuard {
    F f_;
    bool active_{true};

public:
    explicit ScopeGuard(F&& f) : f_(std::forward<F>(f)) {}
    ScopeGuard(ScopeGuard&& o) noexcept
        : f_(std::move(o.f_)), active_(o.active_) { o.active_ = false; }
    ScopeGuard(const ScopeGuard&) = delete;
    ScopeGuard& operator=(const ScopeGuard&) = delete;
    void dismiss() noexcept { active_ = false; }
    ~ScopeGuard() noexcept(std::is_nothrow_invocable_v<F&>) {
        if (active_) f_();
    }
};

template <typename F>
auto make_scope_guard(F&& f) { return ScopeGuard<F>(std::forward<F>(f)); }

Core Guidelines spirit: use RAII first; a scope guard is for ad hoc single-function cleanup when a named type would be too heavy—but a named type is often clearer in public APIs.


RAII in the standard library: ofstream, fstream, and more

Many standard types are RAII by design:

  • std::ifstream / std::ofstream / std::fstream: on destruction, the stream is closed and buffers flushed (you can also close() explicitly before destruction).
  • Smart pointers (above).
  • <mutex> lock types; std::lock algorithm with scoped_lock.
  • Containers (std::vector, std::string, …): they own their storage and free it in their destructors.
  • <thread> std::thread must be joined or detached before destruction, or std::terminate is called—RAII wrappers (or jthread C++20) are strongly recommended to avoid that footgun; this is a case where the type is RAII, but the invariant still requires a correct join model.

<filesystem> path operations are not automatically “transactional,” but a std::ofstream to a path object in a scope is still classic RAII for the open file description.


Anti-patterns I see in code reviews

These are the smells I flag most often—the ones that sound fine until an exception, a bad refactor, or a new contributor adds a third return in the middle of the function.

Throwing in a destructor still shows up. During stack unwinding, another throw from a destructor leads to std::terminate. I ask for a noexcept destructor, swallowing or logging, or a separate close() if failure must be surfaced.

Raw new in the constructor with matching raw delete in the destructor, but a sub-step that can throw after part of the object is live, is a classic partial-construction leak. I push std::unique_ptr per acquisition step, or a factory that only commits the fully built object when nothing can throw along the way.

shared_ptr to this without enable_shared_from_this is a sharp edge: you can get double delete or incoherent ownership. If this must be passed into APIs that need shared_ptr, the object has to be owned by a shared_ptr first, then shared_from_this() is the safe path.

Shallow memcpy of a type that owns a resource is undefined behavior. Copy should be = delete or implemented correctly; move must leave a safe, empty state.

A vector of std::thread with no join policy is a time bomb: ~thread calls std::terminate if the thread is still joinable. I point people at jthread (C++20) or a small wrapper that joins in the destructor.

Two lock guards on the same non-recursive mutex in one scope, or two mutexes taken in different order in different call sites, gives you UB or deadlocks. std::scoped_lock for multiple mutexes, and one documented global order, fixes most of it.

Dismissing a scope guard and then running the same cleanup manually elsewhere doubles work or leaves paths inconsistent. There should be one owner of the “this must run on exit” semantics.

Self-assignment and move must leave the moved-from object in a destructor-safe state—typically null/empty resource.


Performance: the zero-cost abstraction

For unique_ptr, the abstraction is typically identical to a raw pointer plus a compile-time deleter. Inlining and elision often remove any per-operation overhead compared to well-written C.

For mutex guards, the cost is the lock itself, not the guard: one extra stack object with trivial destructor inlining.

For shared_ptr, you pay for atomic refcounting and indirection; where exclusive ownership is enough, prefer unique_ptr.

Core Guidelines P.1: express ideas directly. RAII is both P.1 (intent) and I.1–I.3 (interfaces and invariants) friendly.

Compiler Explorer is useful to verify, for hot paths, that destructor calls inline to a single delete or close when ownership is unique_ptr with a stateless deleter.


Comparison: RAII vs manual management vs garbage collection

Manual new/fclose style management can be as cheap as the hardware allows if you never make a mistake—except errors and exceptions are exactly when humans duplicate cleanup or skip a branch. Deterministic release is possible; in practice it is the first casualty of a growing codebase.

RAII in C++ ties release to scope and a named owner’s lifetime, so the mental question shifts from “where did I free?” to “who owns this type?”. unique_ptr and lock guards are usually minimal runtime overhead; the win is composability under exceptions.

Tracing garbage collectors (e.g. in Java or Go) handle heap memory for you, but file descriptors, sockets, database connections, and OS handles still need a disciplined model—often try/finally or language-specific guards. The pool exhaustion story at the top of this article is a reminder that non-memory resources do not get swept up by a GC.

C++’s model gives control; RAII is how you make that safe and readable.


Advanced patterns: CRTP and policy-based RAII

CRTP (Curiously Recurring Template Pattern) for static polymorphism

Useful when a base provides common acquire/release and a derived class parameterizes a concrete API, without virtual dispatch cost. Example sketch:

template <typename Derived>
struct ResourceBase {
    ~ResourceBase() { static_cast<Derived*>(this)->release(); } // dtor: careful!
};

Caution: calling virtual-like behavior from a base destructor in CRTP is subtle; the usual pattern is to not do heavy logic in a polymorphic dtor, or use composited Pimpl + non-virtual dtor. For templated helpers, prefer composing a policy member (functor deleter) over inheritance from a fragile base destructor.

A safer use of templates for RAII is unique_ptr<T, Deleter>-style: deleter as policy (stateless or stateful, type-erased or not).

#include <cstdio>
#include <memory>

struct PolicyFclose {
    void operator()(std::FILE* f) const noexcept { if (f) std::fclose(f); }
};

struct PolicyNop {
    void operator()(std::FILE*) const noexcept {}
};

// Choose policy for tests vs prod
using UniqueCFile = std::unique_ptr<std::FILE, PolicyFclose>;

Policy-based design

  • Deleter as policy on unique_ptr (lambdas, empty state, custom errors logged).
  • Allocators in containers (allocator = policy for where memory comes from) — separate but analogously composable with RAII types.

CRTP is best for algorithms and traits, not for leaking abstract base* destructor ordering mistakes; composition of policy objects remains the C++ Core Guidelines-friendly default.


Testing RAII: verifying cleanup

  1. Unit tests with mock resources: count construct / destroy in a struct with a test Deleter on unique_ptr or a custom Handle with global/ injected test counters.
  2. Leak detectors: ASan/LSan, Valgrind, static analysis for raw new, clang-tidy checks (cppcoreguidelines-*, clang-analyzer paths).
  3. Exception tests: call a function that throws after resource acquisition; assert counters show one release, no double release.
  4. Thread tests: for locks, use concurrency tests or ThreadSanitizer to catch double lock and data races in tests.

Example: counting deleter for a fake handle:

#include <memory>
struct CountingDeleter {
    int* n{};
    void operator()(int*) const noexcept {
        if (n) ++(*n);
    }
};

void f(int& deletions) {
    std::unique_ptr<int, CountingDeleter> p{new int(1), {&deletions}};
    if (/* failure */) throw 1;
} // dtor always runs: deletions incremented once

Best practices: here’s what I tell new C++ developers

I do not hand them a long checklist. I say: own heap memory with std::make_unique and std::make_shared so subexpressions and constructor ordering cannot surprise you, and the ownership story stays obvious in review (R.14R.15). For exclusive handles, make the type move-only so nobody accidentally copies their way into a double close.

When there is more than one mutex, I push std::scoped_lock and a single, documented lock order—deadlocks in tests are almost always “two sites, two orders.”

Destructors that release must not throw in normal C++ code: mark them noexcept when the platform APIs allow it, and if a close can really fail, I want an explicit close() that returns a status, with the destructor doing best-effort cleanup (C.37).

At module boundaries, I prefer a named RAII type over a clever one-off lambda, so the next reader sees the lifetime contract in the type name. If every member of your class is already a proper RAII type, you often get = default and the Rule of Zero for free, which is fewer hand-written destructors to get wrong (C.20C.22).


Troubleshooting (symptoms and fixes)

std::terminate while unwinding usually means something marked noexcept threw—often a destructor or a scope_guard callback. The fix is to stop throwing from those paths: narrow the callback, catch inside the guard, or move error reporting to a noexcept(true) boundary that logs instead of throws.

“Double free or corruption” in the sanitizer or allocator points to a copy of an owning type, or two owners of the same address. I start by deleting or fixing copy, moving ownership into std::unique_ptr, and removing raw delete from application code.

Leaks that only show up when a constructor throws are almost always subobject order and raw members: wrap each volatile step in a unique_ptr or use a factory that only returns a complete object when acquisition is done.

terminate from ~std::thread means join or detach never ran for a still-joinable thread. std::jthread or a tiny RAII join-on-destruct wrapper ends that class of bug in new code.

Deadlocks in tests with two mutexes: compare call graphs and enforce scoped_lock(a, b) (or a single order everywhere). Same mutex, two guards: that is a different bug—review lock layering.

weak_ptr always “empty” after lock() either means the shared_ptr owner is already gone, or the code is using weak_ptr before anything holds a shared_ptr to the object—lifetime bug, not weak_ptr itself.

vector reallocation feels slow for a large T: if move is not noexcept, the standard library may copy to preserve the strong exception guarantee. For heavy types stored in vector, noexcept move is not optional micro-optimization—it is part of the contract.


Real example: end-to-end (complete working program)

The following single-translation unit program ties together file RAII (C stream with unique_ptr + deleter), mutex + scoped_lock, a Transaction-style rollback guard, a move-only Buffer holding string data, and a ScopeGuard for logging on exit. It is self-contained and suitable for a compile-check with C++17 or later.

// compile: c++ -std=c++17 -O2 raii_demo.cpp -o raii_demo
#include <cstdio>
#include <iostream>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>

// ---- C FILE* RAII (unique_ptr + deleter) ----
struct Fclose {
    void operator()(std::FILE* f) const noexcept { if (f) std::fclose(f); }
};
using FilePtr = std::unique_ptr<std::FILE, Fclose>;

FilePtr make_log_file(const char* path) {
    auto* f = std::fopen(path, "a");
    if (!f) throw std::runtime_error("log open");
    return FilePtr(f);
}

// ---- Transaction stub with rollback in dtor unless commit() ----
struct BankDb {
    std::string state{"INIT"};
    void begin() { state = "IN_TXN"; }
    void commit() { state = "COMMITTED"; }
    void rollback() { state = "ROLLED_BACK"; }
    void touch() { /* write something */ }
};

struct Txn {
    BankDb& db;
    bool done{false};
    explicit Txn(BankDb& b) : db(b) { db.begin(); }
    void commit() { db.commit(); done = true; }
    ~Txn() { if (!done) db.rollback(); }
    Txn(const Txn&) = delete;
    Txn& operator=(const Txn&) = delete;
};

// ---- Move-only buffer: Rule of five minimal ----
struct MsgBuffer {
    std::string data;
    explicit MsgBuffer(std::string s) : data(std::move(s)) {}
};

// ---- Scope guard (simplified) ----
template <typename F>
class ScopeGuard {
    F f_;
    bool on_{true};

  public:
    explicit ScopeGuard(F&& f) : f_(std::forward<F>(f)) {}
    ScopeGuard(ScopeGuard&& o) noexcept
        : f_(std::move(o.f_)), on_(o.on_) { o.on_ = false; }
    ScopeGuard(const ScopeGuard&) = delete;
    ScopeGuard& operator=(const ScopeGuard&) = delete;
    void dismiss() noexcept { on_ = false; }
    ~ScopeGuard() noexcept(std::is_nothrow_invocable_r_v<void, F&>) {
        if (on_) f_();
    }
};
template <typename F>
auto scope_guard(F&& f) { return ScopeGuard<F>(std::forward<F>(f)); }

// ---- Shared data + two mutexes (scoped_lock) ----
struct Shared {
    std::mutex a, b;
    std::vector<std::string> lines;
    void add(const std::string& s) {
        std::scoped_lock lock(a, b);
        lines.push_back(s);
    }
    std::size_t size() {
        std::scoped_lock lock(a, b);
        return lines.size();
    }
};

int main() {
    BankDb bank;
    Shared sh;

    try {
        Txn t(bank);
        t.commit(); // success path: no rollback

        auto f = make_log_file("raii-demo.log");
        std::fputs("start\n", f.get());

        int exit_phase = 0;
        auto g = scope_guard([&exit_phase] {
            std::cout << "scope exit, phase = " << exit_phase << std::endl;
        });

        {
            std::lock_guard<std::mutex> only(sh.a); // example single mutex use
            (void)only;
        }

        sh.add("hello from RAII");

        MsgBuffer m(std::string(64, 'x'));
        (void)m;

        exit_phase = 1;
        g.dismiss(); // cancel logging guard for demo (optional)
    } catch (const std::exception& e) {
        std::cerr << e.what() << '\n';
        return 1;
    }
    return 0;
}

Reading order for maintenance: the Txn destructor runs only if commit was not called—so all or nothing at the database stub level. The FilePtr’s std::fclose runs when f goes out of scope after a successful fopen. The ScopeGuard demonstrates dismissal; in production, you often keep the guard to always log on exit, or never dismiss and keep logic strictly RAII-driven.


Summary

  • RAII ties acquisition to object construction and release to destruction, using C++’s guaranteed unwind order.
  • The C++ standard library already models this with streams, containers, mutex guards, and smart pointers; your own code should do the same for C APIs, sockets, and custom resources.
  • Exception safety is simpler when each resource has one owner and destructors do not throw. Move-only and noexcept move fit vector-friendly strong or basic guarantees at scale.
  • Scope guards complement named RAII types; CRTP and policy deleters are advanced but common in header-only and performance-sensitive code.
  • Test with counters, mocks, sanitizers, and throw paths; when something fails, work through the symptom list in the troubleshooting section and cross-check the Core Guidelines for the specific rule your type is trying to model.
  • C++ design patterns — overview
  • C++ exception safety — concepts
  • C++ smart pointers and RAII
  • C++ unique_ptr / shared_ptr comparison

Keywords

C++ RAII, Resource Acquisition Is Initialization, Core Guidelines, exception safety, unique_ptr, shared_ptr, weak_ptr, scoped_lock, scope guard, move semantics, transaction, destructor noexcept.

One-line summary: In C++, RAII makes resource lifetimes follow object lifetimes so destructors perform reliable, composable cleanup on every exit from a scope, in line with the C++ Core Guidelines for safety and performance.


  • C++ RAII and smart pointers — ownership
  • C++ series — RAII in practice
  • C++ series — design patterns
  • C++ exception basics
  • C++ move semantics