9 RAII and Resource Management
In Gorgo Starting C++ you learned that std::unique_ptr and std::shared_ptr manage memory automatically by freeing it when the pointer goes out of scope. That pattern — acquiring a resource in a constructor and releasing it in a destructor — is one of the most important ideas in C++. It has a name: RAII (Resource Acquisition Is Initialization). RAII applies to far more than memory: file handles, network connections, mutex locks, database transactions, and any other resource that must eventually be released. In this chapter you will learn the RAII pattern in depth, exception safety guarantees, scope guards, and how to use custom deleters with smart pointers.
9.1 The RAII Pattern
RAII ties the lifetime of a resource to the lifetime of an object:
- The constructor acquires the resource.
- The destructor releases the resource.
- Because C++ guarantees that destructors run when objects leave scope (even when exceptions are thrown), the resource is always released.
Here is RAII applied to a file handle:
#include <cstdio>
#include <stdexcept>
#include <string>
class FileHandle {
public:
FileHandle(const std::string& path, const char* mode)
: fp_(std::fopen(path.c_str(), mode)) {
if (!fp_) {
throw std::runtime_error("Cannot open: " + path);
}
}
~FileHandle() {
if (fp_) {
std::fclose(fp_);
}
}
// Prevent copying (two objects should not close the same file)
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// Allow moving
FileHandle(FileHandle&& other) noexcept : fp_(other.fp_) {
other.fp_ = nullptr;
}
FILE* get() const { return fp_; }
private:
FILE* fp_;
};With this class, a file is always closed, no matter how the function exits:
void process_file(const std::string& path) {
FileHandle file(path, "r");
// ... use file.get() ...
// If an exception is thrown here, ~FileHandle still runs
}
// ~FileHandle runs when 'file' goes out of scopeWithout RAII, you would need to remember to call fclose() on every exit path — including the ones created by exceptions.
Tip: If you find yourself writing cleanup code in multiple places, you probably need an RAII wrapper. If the resource already has a standard wrapper (like std::fstream for files, std::lock_guard for mutexes), use that instead of writing your own.
9.1.1 RAII Beyond Memory
RAII works with any resource:
| Resource | RAII Wrapper |
|---|---|
| Heap memory | std::unique_ptr, std::shared_ptr |
| Files | std::fstream, std::ofstream, std::ifstream |
| Mutex locks | std::lock_guard, std::unique_lock |
| Database connections | Custom wrapper |
| Network sockets | Custom wrapper |
| Temporary files | Custom wrapper (create in ctor, delete in dtor) |
9.2 Exception Safety Guarantees
When a function throws an exception, what state does it leave the program in? C++ defines three levels of exception safety:
9.2.1 Basic Guarantee
The basic guarantee promises:
- No resources are leaked.
- The program is in a valid state (no undefined behavior).
- But the state may have changed — partial modifications may be visible.
Most well-written C++ code provides at least the basic guarantee. RAII gives you this almost for free: if every resource is managed by an object, destructors clean up automatically.
9.2.2 Strong Guarantee
The strong guarantee promises:
- If the function throws, the program state is unchanged — as if the function had never been called.
- This is “commit or rollback” semantics.
Providing the strong guarantee usually means doing all work on a copy, then swapping:
void update_playlist(std::vector<std::string>& playlist, const std::string& song) {
std::vector<std::string> temp = playlist; // copy
temp.push_back(song); // modify copy
// If push_back throws (bad_alloc), playlist is untouched
playlist = std::move(temp); // commit (noexcept)
}9.2.3 Nothrow Guarantee
The nothrow guarantee promises the function never throws. Mark such functions with noexcept:
void swap(int& a, int& b) noexcept {
int temp = a;
a = b;
b = temp;
}Destructors are implicitly noexcept. Move constructors and move assignment operators should be noexcept whenever possible — the standard library containers rely on this for efficiency.
Trap: If a noexcept function does throw, std::terminate is called and the program crashes. Only use noexcept when you are certain the function cannot throw.
9.2.4 Custom Exception Classes
The standard exception hierarchy (std::runtime_error, std::logic_error, etc.) covers a lot of ground, but real applications usually want their own exception types so callers can catch their errors specifically and not get tangled with library exceptions:
#include <stdexcept>
#include <string>
class ConfigError : public std::runtime_error {
public:
explicit ConfigError(const std::string& msg)
: std::runtime_error("config: " + msg) {}
};
class NetworkError : public std::runtime_error {
public:
NetworkError(const std::string& host, int port)
: std::runtime_error("network: " + host + ":" + std::to_string(port)),
host_(host), port_(port) {}
const std::string& host() const { return host_; }
int port() const { return port_; }
private:
std::string host_;
int port_;
};Two rules make custom exception classes worth the effort:
- Inherit from
std::exception(or a subclass of it). Code that doescatch (const std::exception& e)keeps working, ande.what()returns the message you passed to the base constructor. - Derive from
std::runtime_errororstd::logic_error. They both already implement thewhat()plumbing for you and store the message string. Inheriting directly fromstd::exceptionmeans writing your ownwhat()— doable, but unnecessary churn.
Catching the most-derived type a caller cares about is then natural:
try {
load();
} catch (const NetworkError& e) {
retry_later(e.host(), e.port());
} catch (const ConfigError& e) {
bail_out(e.what());
} catch (const std::exception& e) {
log_unknown(e.what());
}
Tip: Exception destructors must not throw. The base classes you should be inheriting from (std::runtime_error, std::logic_error) already mark their destructors noexcept; do not override that.
9.2.5 Which Guarantee to Aim For
| Situation | Recommendation |
|---|---|
| Destructors | Always nothrow |
| Move operations | Nothrow whenever possible |
| Simple operations | Basic guarantee is usually sufficient |
| Operations that modify shared state | Consider strong guarantee |
| Swap functions | Nothrow |
9.3 Scope Guards
Sometimes you need cleanup that does not fit neatly into a class destructor. A scope guard is a small RAII object that runs a function when it goes out of scope:
#include <functional>
#include <iostream>
class ScopeGuard {
public:
explicit ScopeGuard(std::function<void()> cleanup)
: cleanup_(std::move(cleanup)) {}
~ScopeGuard() {
if (cleanup_) {
cleanup_();
}
}
void dismiss() { cleanup_ = nullptr; }
ScopeGuard(const ScopeGuard&) = delete;
ScopeGuard& operator=(const ScopeGuard&) = delete;
private:
std::function<void()> cleanup_;
};Usage:
void process() {
auto* raw = acquire_resource();
ScopeGuard guard([raw]() {
release_resource(raw);
std::cout << "Resource released\n";
});
// ... do work that might throw ...
// If we get here successfully, maybe we want to keep the resource:
// guard.dismiss(); // cancel the cleanup
}
// guard's destructor releases the resource if not dismissedThe dismiss() method lets you cancel the cleanup if the operation succeeds — useful for commit/rollback patterns.
Tip: The C++ standard library does not have a scope guard yet, but <experimental/scope> provides scope_exit, scope_success, and scope_fail in some implementations. Writing your own is straightforward, as shown above.
9.4 Custom Deleters with Smart Pointers
std::unique_ptr and std::shared_ptr call delete by default, but you can provide a custom deleter for resources that need different cleanup.
9.4.1 unique_ptr with Custom Deleter
The deleter is part of the type:
#include <cstdio>
#include <iostream>
#include <memory>
int main() {
auto file_deleter = [](FILE* fp) {
if (fp) {
std::fclose(fp);
std::cout << "File closed\n";
}
};
std::unique_ptr<FILE, decltype(file_deleter)> file(
std::fopen("playlist.txt", "w"), file_deleter);
if (file) {
std::fprintf(file.get(), "1. Clocks\n2. Yellow\n");
}
return 0;
}
// file_deleter runs here, closing the fileFile closed 9.4.2 shared_ptr with Custom Deleter
With shared_ptr, the deleter is not part of the type — you pass it as a constructor argument:
#include <cstdlib>
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<void> memory(
std::malloc(1024),
[](void* ptr) {
std::free(ptr);
std::cout << "Memory freed\n";
}
);
// Use memory.get() ...
return 0;
}Memory freed9.4.3 Practical Example: C Library Handles
Many C libraries return opaque handles that must be freed with a specific function. Custom deleters let you manage them with smart pointers:
// Example with a hypothetical C library
// Handle create_session();
// void destroy_session(Handle h);
auto deleter = [](Handle* h) { destroy_session(*h); delete h; };
std::unique_ptr<Handle, decltype(deleter)> session(
new Handle(create_session()), deleter);
Tip: When wrapping C library resources, prefer unique_ptr with a custom deleter. It has zero overhead compared to manual cleanup and guarantees the resource is released exactly once.
9.5 Common Idioms
A handful of idioms come up often enough that they have names. They are not new language features — they are recipes built from the building blocks you already know.
9.5.1 Copy-and-Swap
You know from Gorgo Starting C++ that the copy assignment operator has to handle three things at once: free the existing resources, copy the source’s resources, and survive self-assignment. Doing all three by hand is a maze of if (this != &other) checks and partial-update bugs.
The copy-and-swap idiom sidesteps the whole maze. It builds a temporary copy of the source, then swaps it with *this:
class Buffer {
int* data_;
std::size_t size_;
public:
Buffer(const Buffer& other); // standard copy ctor
Buffer( Buffer&& other) noexcept; // standard move ctor
~Buffer();
Buffer& operator=(Buffer other) { // takes by value: copy or move
std::swap(data_, other.data_);
std::swap(size_, other.size_);
return *this; // other's destructor cleans up the old data
}
};The parameter is taken by value, so the compiler picks the copy or move constructor automatically depending on the call site. The two std::swap calls exchange the new values into *this; when other goes out of scope, its destructor frees what used to be *this’s old data.
Three properties fall out for free: it handles self-assignment correctly (you swap with a copy, not the original), it provides the strong exception guarantee (if the copy throws, *this is untouched), and it deduplicates the copy and move assignment operators into one body.
Tip: Copy-and-swap pairs especially well with the rule of zero — if every resource your class holds is itself an RAII type (std::vector, std::unique_ptr, etc.), you do not even need to write the copy and move constructors; the compiler-generated ones suffice.
9.5.2 Factory Pattern
Sometimes a constructor is the wrong API. Maybe construction can fail and you want to return std::optional<T> instead of throwing; maybe the caller should not be aware of which concrete subclass they get back; maybe the construction process needs work that does not fit cleanly in an initializer list. A factory function sidesteps the constructor altogether:
class Connection {
Connection() = default; // private or undocumented
public:
static std::optional<Connection> open(const std::string& url);
// ... methods ...
};
std::optional<Connection> Connection::open(const std::string& url) {
if (url.empty()) return std::nullopt;
Connection c;
// ... do the connection setup ...
return c;
}std::make_unique<T>() and std::make_shared<T>() are the standard library’s most-used factories. The pattern is “name the operation, not the type” — Connection::open("...") reads like the action it performs, where Connection("...") reads like a noun.
9.5.3 Singleton (Meyers Singleton)
When exactly one instance of a class should exist for the whole program (a logger, a configuration registry, a connection pool), a singleton ensures exactly one is constructed and reused. The cleanest C++ form is the Meyers singleton — a function-local static:
class Logger {
public:
static Logger& instance() {
static Logger inst; // created on first call, thread-safe since C++11
return inst;
}
void log(const std::string&) { /* ... */ }
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
private:
Logger() = default;
};The function-local static is constructed the first time instance() is called and destroyed at program exit. The C++11 standard guarantees the construction is thread-safe, so two threads racing to call instance() will not produce two instances.
The deleted copy constructor and copy assignment operator are what make the type not duplicable — without them, a caller could write Logger backup = Logger::instance(); and create a second instance.
Trap: Singletons are the tempting design that ages worst. They are global state with extra ceremony: hard to test, hard to mock, and quietly couple every module that uses them. Use one only when you genuinely have a system-wide resource that must not be duplicated — not just to avoid threading parameters around.
9.5.4 Strategy via Lambdas
The classic strategy pattern uses an interface class with a virtual method, plus a concrete subclass per algorithm. In modern C++, a std::function<...> parameter (or even a template parameter) accomplishes the same goal in one line:
#include <algorithm>
#include <functional>
#include <vector>
class Sorter {
std::function<bool(int, int)> compare_;
public:
explicit Sorter(std::function<bool(int, int)> cmp) : compare_(std::move(cmp)) {}
void sort(std::vector<int>& v) {
std::sort(v.begin(), v.end(), compare_);
}
};
Sorter ascending ([](int a, int b) { return a < b; });
Sorter descending([](int a, int b) { return a > b; });No interface class, no concrete subclass, no inheritance. Each “strategy” is just a lambda the caller hands in. For inner-loop hot paths, replace std::function with a template parameter (template<typename Cmp>) so the compiler can inline through the call.
9.5.5 Observer / Signal-Slot
The observer pattern lets one object notify many others when something happens. The minimal C++ form is a std::vector<std::function<...>> of subscribers:
#include <functional>
#include <iostream>
#include <string>
#include <vector>
class TrackPlayer {
std::vector<std::function<void(const std::string&)>> listeners_;
public:
void on_track_change(std::function<void(const std::string&)> cb) {
listeners_.push_back(std::move(cb));
}
void play(const std::string& title) {
for (auto& cb : listeners_) cb(title);
}
};
int main() {
TrackPlayer player;
player.on_track_change([](const std::string& t) { std::cout << "now: " << t << "\n"; });
player.on_track_change([](const std::string&) { /* update UI */ });
player.play("Single Ladies");
return 0;
}For richer needs — unsubscription, type-safe signal IDs, threaded delivery — libraries like Boost.Signals2 or sigslot offer feature-complete versions. But for the everyday “broadcast an event to a list of callbacks,” the vector-of-function form is enough and brings no dependencies.
9.6 Compile-Time and Run-Time Type Inspection
You met typeid and dynamic_cast briefly with RTTI in Chapter 1; this section takes a closer look at both run-time inspection and its compile-time counterpart. decltype and typeid answer two related questions: “what is the static type of this expression?” and “what is the dynamic type of this object?”
9.6.1 decltype
decltype(expr) yields the type of the expression at compile time. It is what you reach for when you need to declare a variable that has the same type as something else, or when you need to write a return type that depends on the parameter types (you saw this with trailing return types in Chapter 2):
int x = 5;
decltype(x) y = 10; // y is int
const int& cref = x;
decltype(cref) z = x; // z is const int& --- reference and constness preserved
template<typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
return a + b;
}decltype differs from auto in two important ways: it preserves references and constness exactly (auto strips both), and it works on expressions you do not actually evaluate. decltype(some_function()) gives you the return type of some_function without calling it.
9.6.2 typeid
typeid(x) returns a reference to a std::type_info object (const std::type_info&) describing the runtime type of x (for polymorphic types) or its static type (for non-polymorphic types). You need #include <typeinfo>:
#include <iostream>
#include <typeinfo>
class Animal { public: virtual ~Animal() = default; };
class Cat : public Animal {};
int main() {
Cat cat;
Animal& ref = cat;
std::cout << typeid(int).name() << "\n";
std::cout << typeid(ref).name() << "\n"; // prints Cat (mangled), not Animal
std::cout << (typeid(ref) == typeid(Cat)) << "\n"; // 1
return 0;
}For polymorphic types, typeid reports the most-derived type the reference or pointer actually refers to. For non-polymorphic types, it reports the declared type. type_info::name() is implementation-defined and most useful for debug printing.
Tip: Reach for dynamic_cast (Chapter 1) when you want to use the derived type, and typeid when you only need to compare types. The two are different tools for different jobs — typeid does not do conversions.
9.7 Try It: RAII in Action
Here is a program that demonstrates RAII with different resource types. Type it in, compile with g++ -std=c++23, and experiment:
#include <cstdio>
#include <functional>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
// Simple scope guard
class ScopeGuard {
public:
explicit ScopeGuard(std::function<void()> fn) : fn_(std::move(fn)) {}
~ScopeGuard() { if (fn_) fn_(); }
void dismiss() { fn_ = nullptr; }
ScopeGuard(const ScopeGuard&) = delete;
ScopeGuard& operator=(const ScopeGuard&) = delete;
private:
std::function<void()> fn_;
};
// RAII file wrapper using unique_ptr with custom deleter
using FilePtr = std::unique_ptr<FILE, decltype([](FILE* f) { std::fclose(f); })>;
FilePtr open_file(const std::string& path, const char* mode) {
FILE* fp = std::fopen(path.c_str(), mode);
if (!fp) throw std::runtime_error("Cannot open: " + path);
return FilePtr(fp);
}
int main() {
// RAII file handle
try {
auto file = open_file("/tmp/raii_test.txt", "w");
std::fprintf(file.get(), "Somebody That I Used to Know\n");
std::cout << "Wrote to file\n";
} catch (const std::exception& e) {
std::cout << "Error: " << e.what() << "\n";
}
// file is automatically closed here
// Scope guard
std::cout << "Starting operation...\n";
{
ScopeGuard guard([]() {
std::cout << "Cleanup complete\n";
});
std::cout << "Doing work...\n";
// guard.dismiss(); // uncomment to skip cleanup
}
// guard runs cleanup here
// shared_ptr with custom deleter
{
auto ptr = std::shared_ptr<int>(
new int(42),
[](int* p) {
std::cout << "Custom delete: " << *p << "\n";
delete p;
}
);
std::cout << "Value: " << *ptr << "\n";
}
return 0;
}Try adding a function that throws an exception after opening a file and verify the file is still closed. Try the scope guard with dismiss() to see the difference.
9.8 Key Points
- RAII (Resource Acquisition Is Initialization) ties resource lifetime to object lifetime. The constructor acquires, the destructor releases.
- C++ guarantees destructors run when objects leave scope, even during exception unwinding. This makes RAII the foundation of exception-safe code.
- The basic guarantee promises no leaks and valid state but allows partial modifications.
- The strong guarantee promises rollback on failure (copy, modify, swap).
- The nothrow guarantee (
noexcept) promises no exceptions. Use it for destructors, moves, and swaps. - Scope guards are lightweight RAII objects that run a cleanup function at scope exit.
dismiss()can cancel the cleanup for commit/rollback patterns. - Custom deleters let
unique_ptrandshared_ptrmanage non-memory resources (file handles, C library objects). unique_ptrcustom deleters are part of the type;shared_ptrcustom deleters are not.- When wrapping C resources, prefer
unique_ptrwith a custom deleter — zero overhead, guaranteed cleanup.
9.9 Exercises
Think about it: Why is RAII considered one of the most important patterns in C++? How does it compare to try/finally in languages like Java and Python?
What happens here?
void risky() { FILE* fp = fopen("data.txt", "r"); process(fp); // might throw fclose(fp); }What goes wrong if
processthrows an exception? How would you fix it with RAII?Think about it: Why should move constructors and move assignment operators be
noexcept? What happens if they are not?Where is the bug?
class Connection { public: Connection() { connect(); } ~Connection() { disconnect(); } }; void transfer() { Connection c1; Connection c2 = c1; // copy // ... work ... }Calculation: How many times is
fclosecalled?{ auto d = [](FILE* f) { fclose(f); }; std::unique_ptr<FILE, decltype(d)> f1(fopen("a.txt", "r"), d); auto f2 = std::move(f1); }Think about it: What is the difference between the basic guarantee and the strong guarantee? Give an example where the basic guarantee is sufficient and one where you would want the strong guarantee.
Where is the bug?
void process() { auto ptr = std::make_unique<int[]>(100); // ... do work ... ptr.release(); // "release" the memory }What does this print?
{ ScopeGuard g1([]() { std::cout << "A "; }); ScopeGuard g2([]() { std::cout << "B "; }); ScopeGuard g3([]() { std::cout << "C "; }); }(Using the ScopeGuard class from this chapter.)
Think about it: Why does
shared_ptrnot include the deleter in its type, whileunique_ptrdoes? What design trade-off does this represent?Write a program that wraps
malloc/freein aunique_ptrwith a custom deleter. Allocate an array of 10 integers, fill them with values, print them, and verify the memory is freed by printing a message in the deleter.Where is the bug?
class CacheError : public std::exception { public: CacheError(const std::string& key) : key_("cache miss: " + key) {} const char* what() const noexcept override { return key_.c_str(); } private: std::string key_; };The class compiles, but it is missing the small refinement that the chapter recommends. Rewrite it the recommended way and explain what you bought.
Think about it: The Meyers singleton uses a function-local
static. Why is the function form preferred over astaticdata member at namespace scope? Mention what changed in C++11 that made this idiom safe in multithreaded programs.What does this print?
#include <iostream> #include <typeinfo> class Animal { public: virtual ~Animal() = default; }; class Cat : public Animal {}; class Dog : public Animal {}; int main() { Cat c; Animal& a1 = c; Animal* a2 = new Dog(); std::cout << (typeid(a1) == typeid(Cat)) << " " << (typeid(a1) == typeid(Dog)) << " " << (typeid(*a2) == typeid(Dog)) << " " << (typeid(a2) == typeid(Dog*)) << "\n"; delete a2; return 0; }Pay attention to which expressions are dereferenced and which are not.
What is the deduced type of each variable?
int x = 5; const int& cref = x; auto a = cref; // (a) auto& b = cref; // (b) decltype(cref) c = cref; // (c) decltype(x) d = cref; // (d)Explain the rules behind each deduction.