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.
| Resource | RAII wrapper | Acquires / Releases |
|---|---|---|
| Heap memory (unique) | std::unique_ptr<T> | new / delete |
| Heap memory (shared) | std::shared_ptr<T> | new / delete (ref-counted) |
| Mutex lock | std::lock_guard<Mutex> | mutex.lock() / mutex.unlock() |
| Flexible mutex lock | std::unique_lock<Mutex> | lock() / unlock() (deferred, movable) |
| File | std::fstream / std::ifstream | open / close + flush |
| Thread | std::jthread (C++20) | create / join |
| Dynamic array | std::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.
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.
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.
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 automaticallyScope 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).