Resource handling is one of the most common sources of bugs in C++. Memory leaks, deadlocks, and exhausted file descriptors are often the result of naive manual resource management.
RAII (Resource Acquisition Is Initialization) is the C++ mechanism that makes resource management reliable. The idea is simple: acquire the resource in a constructor and release it in a destructor. Destructors run automatically when objects leave scope — even if the function exits via an exception.
The C++ standard library heavily relies on RAII: containers manage memory, std::lock_guard manages mutex locking, and std::fstream manages file handles.
Why do we need RAII?
A common “old-school” approach is to store a raw pointer in a class and delete it in the destructor.
For example, UserController takes a pointer to a polymorphic Database object and calls init() in the constructor:
#include <stdexcept>
struct Database {
virtual ~Database() = default;
virtual void init() = 0;
};
struct PostgresDB : Database {
void init() override {
throw std::runtime_error("DB init failed");
}
};
class UserController {
public:
explicit UserController(Database* db)
: m_database(db)
{
// If init() throws, UserController's destructor will NOT run.
// m_database is a raw pointer -> memory leak.
m_database->init();
}
~UserController() {
delete m_database;
}
private:
Database* m_database = nullptr;
};
At first glance, this looks reasonable: the class “owns” the pointer and deletes it in the destructor.
The problem appears when init() throws. If a constructor fails, the object is not considered fully constructed, so the runtime does not call the destructor of the object itself. It only destroys the members that were successfully constructed. A raw pointer is not an RAII object, so nothing releases it — leak.
This is how real-world leaks happen: a small assumption, an exceptional path, and you get a bug that is hard to reproduce and debug.
Manual ownership forces you into the Rule of Five
If a class owns a resource manually (e.g., through a raw pointer), it must define a correct ownership policy for copying and moving. That usually means implementing the “Rule of Five”:
- destructor
- copy constructor
- copy assignment operator
- move constructor
- move assignment operator
All that required to enable proper move and copy semantic for the general class UserController among business logic. For example, this class may disable copy and enable only move semantic:
class UserController {
public:
explicit UserController(Database* db);
// disable copy semantic
UserController(const UserController& other) = delete;
UserController& operator=(const UserController& other);
// enable only move semantic
UserController(UserController&& other): m_database(other.m_database){
// don't forget to make this pointer `null` to prevent double deletion
other.m_database = nullptr;
}
UserController& operator=(UserController&& other){
// don't forget to release memory to prevent the leak
delete m_database;
m_database = other.m_database;
// don't forget to make this pointer `null` to prevent double deletion
other.m_database = nullptr;
}
~UserController() {
delete m_database;
}
private:
Database* m_database = nullptr;
};
This is a lot of code especially for a single resource – m_database — and it’s easy to get wrong. But think about having more resources: log file, socket and others. Managing them manually in the general class means to introducing additional risk of bugs.
Default copy is dangerous (shallow copy)
If you rely on compiler-generated copy operations, you get a shallow copy of the pointer:
class BadUserController {
public:
explicit BadUserController(Database* db) : m_database(db) {}
~BadUserController() { delete m_database; }
private:
Database* m_database = nullptr;
};
// Somewhere in code:
BadUserController a(new PostgresDB{});
BadUserController b = a; // shallow copy: both point to the same object
// Double delete on destruction -> undefined behavior
The diagram bellow depicts the overall situation after the copy:

Two objects end up owning the same resource. When one is destroyed, the other holds a dangling pointer. The second destruction becomes a double delete — undefined behavior.
How RAII works
RAII relies on one rule: destructors run when objects leave scope.
This is true for normal returns and for exceptions. C++ guarantees stack unwinding: local objects are destroyed in reverse order of construction.
The RAII pattern is straightforward:
- acquire a resource in a constructor,
- release it in a destructor,
- let scope and lifetime handle cleanup automatically.
This eliminates the need to “remember” cleanup paths, handle every early return manually, or worry about exceptions skipping cleanup code.
Example: file handling with std::ifstream
std::ifstream is an RAII wrapper around a file handle:
#include <fstream>
#include <string>
std::string read_first_line(const std::string& path) {
std::ifstream file(path); // acquire
if (!file) return {};
std::string line;
std::getline(file, line); // use
return line; // release happens in ~ifstream()
}
No explicit close() is required. The file is closed automatically.
Fixing UserController with std::unique_ptr
The correct fix is to wrap the raw pointer in an RAII owner: std::unique_ptr.
#include <memory>
class UserController {
public:
explicit UserController(std::unique_ptr<Database> db)
: m_database(std::move(db))
{
// If init() throws, m_database is already constructed,
// and ~unique_ptr will release the resource during unwinding.
m_database->init();
}
private:
std::unique_ptr<Database> m_database;
};
Now, if init() throws, C++ destroys already constructed members. std::unique_ptr releases the owned Database object in its destructor, and the leak disappears.
If you enjoy “under the hood” explanations like this, I share one deep dive per month in my newsletter From complexity to essence in C++.
New subscribers get a free bonus guide: Object lifetime in C++ — copy vs move, special member functions, and how RAII makes resource management safe.
Proper RAII design
RAII is not only about freeing a resource in a destructor. A good RAII wrapper also defines correct copy/move semantics — or intentionally disables copying when copying doesn’t make sense.
This matches the Single Responsibility Principle: the wrapper’s job is resource ownership, including:
- how the resource is released,
- whether it can be copied,
- how it is moved.
For example, std::unique_ptr is an RAII wrapper for a raw pointer:
- copying is disabled (unique ownership),
- moving is enabled (transfer ownership).
namespace std {
/// 20.7.12.2 unique_ptr for single objects.
template <typename _Tp, typename _Tp_Deleter = default_delete<_Tp> >
class unique_ptr
{
// ....
// Move constructors.
unique_ptr(unique_ptr&& __u)
: _M_t(__u.release(), std::forward<deleter_type>(__u.get_deleter())) { }
template<typename _Up, typename _Up_Deleter>
unique_ptr(unique_ptr<_Up, _Up_Deleter>&& __u)
: _M_t(__u.release(), std::forward<deleter_type>(__u.get_deleter()))
{ }
// Assignment.
unique_ptr&
operator=(unique_ptr&& __u)
{
reset(__u.release());
get_deleter() = std::move(__u.get_deleter());
return *this;
}
template<typename _Up, typename _Up_Deleter>
unique_ptr&
operator=(unique_ptr<_Up, _Up_Deleter>&& __u)
{
reset(__u.release());
get_deleter() = std::move(__u.get_deleter());
return *this;
}
// Disable copy from lvalue.
unique_ptr(const unique_ptr&) = delete;
template<typename _Up, typename _Up_Deleter>
unique_ptr(const unique_ptr<_Up, _Up_Deleter>&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
template<typename _Up, typename _Up_Deleter>
unique_ptr& operator=(const unique_ptr<_Up, _Up_Deleter>&) = delete;
private:
__tuple_type _M_t;
};
} // namespace std
Because ownership is encapsulated inside std::unique_ptr, classes that use it can often follow the Rule of Zero: write no custom destructor/copy/move operations and rely on compiler defaults.
Rewritten UserController doesn’t need any special member functions:
#include <memory>
class UserController {
public:
explicit UserController(std::unique_ptr<Database> db)
: m_database(std::move(db))
{
m_database->init();
}
private:
std::unique_ptr<Database> m_database;
};
This approach makes the program simpler, safer, and easier to reason about — because the cleanup logic is guaranteed by object lifetimes, not by discipline.
Summary
- Manual resource management is fragile and exception-unsafe.
- RAII makes cleanup automatic and reliable.
- Prefer standard RAII types:
std::unique_ptr,std::lock_guard,std::fstream, containers. - Aim for the Rule of Zero: let RAII members define correct destruction and ownership.