RAII is C++'s core habit: **resource acquisition is initialization**. Every resource is tied to an object, so the resource is acquired when the object is constructed and released when the object is destroyed, automatically, on every path out of the scope. Because C++ guarantees that destructors run even when an exception unwinds the stack, RAII makes leaks and inconsistent state nearly impossible.
The idea in one sentence
Wrap a resource in a class. The constructor acquires it; the destructor releases it. Then use the object, and let its lifetime do the work.
#include
#include
#include
class File {
public:
explicit File(const std::string& path) : file_(path, std::ios::out) {
if (!file_) {
std::cerr << "could not open " << path << '\n';
}
}
~File() {
if (file_.is_open()) {
file_.close();
}
}
void write_line(const std::string& text) {
file_ << text << '\n';
}
bool good() const { return file_.is_open(); }
private:
std::fstream file_;
};
int main() {
{
File log("records.txt");
if (!log.good()) {
std::cerr << "giving up\n";
return 1;
}
log.write_line("first record");
log.write_line("second record");
}
std::cout << "file closed automatically\n";
return 0;
}The output shows the file being closed without a single explicit close call:
file closed automaticallyThe file was opened in the constructor and closed in the destructor, and the destructor ran when the block ended.
Why destructors run on the error path
The reason RAII is not merely a convenience is the exception path. When an exception propagates, C++ unwinds the stack, running the destructor of every object in the frames it leaves.
#include
#include
class Guard {
public:
explicit Guard(const std::string& label) : label_(label) {
std::cout << "acquired " << label_ << '\n';
}
~Guard() {
std::cout << "released " << label_ << '\n';
}
private:
std::string label_;
};
void risky() {
Guard guard("database connection");
throw std::runtime_error("query failed");
}
int main() {
try {
risky();
} catch (const std::exception& error) {
std::cout << "caught: " << error.what() << '\n';
}
return 0;
}The output shows the guard being released even though the function exited by throwing:
acquired database connection
released database connection
caught: query failedWithout RAII, the connection would have been left open. This is the single strongest argument for the pattern: it is exception-safe by construction.
RAII for every kind of resource
The pattern is not limited to memory. It works for anything that has to be given back.
- Memory —
std::unique_ptr,std::vector,std::string. - Files —
std::fstreamcloses itself in its destructor. - Locks —
std::lock_guardreleases the mutex when the scope ends. - Transactions — a rollback object that undoes the transaction in its destructor unless you commit it.
- Raw handles — a class that wraps a C file handle and closes it.
#include
#include
#include
class Transaction {
public:
explicit Transaction(const std::string& name) : name_(name) {
std::cout << "begin " << name_ << '\n';
}
void commit() {
committed_ = true;
std::cout << "commit " << name_ << '\n';
}
~Transaction() {
if (!committed_) {
std::cout << "rollback " << name_ << '\n';
}
}
private:
std::string name_;
bool committed_ = false;
};
int main() {
{
Transaction good("transfer");
good.commit();
}
{
Transaction bad("audit");
// no commit: the destructor rolls it back
}
return 0;
}The output shows the two outcomes:
begin transfer
commit transfer
begin audit
rollback auditThe three rules of RAII
One resource, one object. If a class owns two resources, its destructor has two chances to get one of them wrong. Split it into two classes and compose them.
Acquire in the constructor, release in the destructor. Nothing should be half-acquired when the constructor finishes, and nothing should be released before the destructor begins.
Scope as narrowly as possible. The resource is released when the scope ends, so make the scope short. A lock held for a whole function is a lock held far too long.
#include
#include
class ScopedName {
public:
explicit ScopedName(const std::string& name) : previous_(name) {
std::cout << "entering " << name << '\n';
}
~ScopedName() {
std::cout << "leaving " << previous_ << '\n';
}
private:
std::string previous_;
};
void process() {
ScopedName scope("processing");
std::cout << "working\n";
}
int main() {
process();
std::cout << "done\n";
return 0;
}The output shows the scope being entered and left exactly once:
entering processing
working
leaving processing
doneRAII and smart pointers
A smart pointer is RAII applied to memory. The constructor allocates, the destructor frees, and the unique_ptr and shared_ptr types add the ownership rules on top.
That is why the modern advice is not "be careful with delete" but "let an object own the resource". Once ownership is expressed in a type, the rest of the language takes care of the details.
Where RAII comes from
The name is not decoration. RAII was the mechanism C++ adopted from the very beginning so that objects could own resources, and it is why a C++ object can represent an open file, a held lock or a reserved block of memory without any special support from the language.
The idea generalises to anything with a lifetime: a database transaction, a temporary directory, a lease on a licence, a lock on a mutex. In each case there is an acquire step and a release step, and the release step must happen exactly once, no matter how the code in between leaves the scope.
The difference between RAII and a manual cleanup block at the end of a function is the number of exit paths. A function has more exits than you think: the normal end, an early return, a throw, a break out of a loop that contains the resource, and a return from a nested call. RAII covers all of them at once, because the destructor is attached to the object rather than to a line of code.
RAII and containers
Every standard container is a RAII type. A std::vector allocates in its constructor, grows as you add elements, and frees everything in its destructor. A std::string does the same for characters. A std::unique_ptr does it for a single object.
That is why the standard library's containers are safe to use inside functions that throw, and why a vector declared before a risky call does not leak when the call fails.
#include
#include
#include
#include
std::vector build_records(int count) {
std::vector records;
records.reserve(count);
for (int i = 0; i < count; ++i) {
if (i == 3) {
throw std::runtime_error("record could not be created");
}
records.push_back(i * 10);
}
return records;
}
int main() {
try {
build_records(10);
} catch (const std::exception& error) {
std::cout << "caught: " << error.what() << '\n';
}
return 0;
}The output shows the exception being caught, and the partially built vector having been cleaned up on the way out:
caught: record could not be createdNothing had to be cleaned up by hand, and there is no path through this program that can leak.
Quick reference
- RAII means a resource is acquired in a constructor and released in a destructor.
- Destructors run on every exit path, including when an exception unwinds.
- It applies to memory, files, locks, transactions and any handle that must be given back.
- One resource per object; compose rather than combining.
- Keep the scope as narrow as possible so the resource is released early.
- Smart pointers are RAII for memory, and they are the default for dynamic allocation.
Try it yourself: write a small RAII class that opens a file in its constructor and closes it in its destructor, then use it inside a function that throws halfway through. Confirm the file is closed by checking that you can delete it immediately afterwards on Windows. Next, add a [[commit]] flag so the destructor only rolls back an uncommitted transaction, and try both paths.