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:

  1. The constructor acquires the resource.
  2. The destructor releases the resource.
  3. 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 scope

Without 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 does catch (const std::exception& e) keeps working, and e.what() returns the message you passed to the base constructor.
  • Derive from std::runtime_error or std::logic_error. They both already implement the what() plumbing for you and store the message string. Inheriting directly from std::exception means writing your own what() — 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 dismissed

The 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 file
File 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 freed

9.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_ptr and shared_ptr manage non-memory resources (file handles, C library objects).
  • unique_ptr custom deleters are part of the type; shared_ptr custom deleters are not.
  • When wrapping C resources, prefer unique_ptr with a custom deleter — zero overhead, guaranteed cleanup.

9.9 Exercises

  1. 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?

  2. What happens here?

    void risky() {
        FILE* fp = fopen("data.txt", "r");
        process(fp);  // might throw
        fclose(fp);
    }

    What goes wrong if process throws an exception? How would you fix it with RAII?

  3. Think about it: Why should move constructors and move assignment operators be noexcept? What happens if they are not?

  4. Where is the bug?

    class Connection {
    public:
        Connection() { connect(); }
        ~Connection() { disconnect(); }
    };
    
    void transfer() {
        Connection c1;
        Connection c2 = c1;  // copy
        // ... work ...
    }
  5. Calculation: How many times is fclose called?

    {
        auto d = [](FILE* f) { fclose(f); };
        std::unique_ptr<FILE, decltype(d)> f1(fopen("a.txt", "r"), d);
        auto f2 = std::move(f1);
    }
  6. 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.

  7. Where is the bug?

    void process() {
        auto ptr = std::make_unique<int[]>(100);
        // ... do work ...
        ptr.release();  // "release" the memory
    }
  8. 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.)

  9. Think about it: Why does shared_ptr not include the deleter in its type, while unique_ptr does? What design trade-off does this represent?

  10. Write a program that wraps malloc/free in a unique_ptr with 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.

  11. 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.

  12. Think about it: The Meyers singleton uses a function-local static. Why is the function form preferred over a static data member at namespace scope? Mention what changed in C++11 that made this idiom safe in multithreaded programs.

  13. 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.

  14. 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.