RAII: Resources That Clean Up After Themselves

RAII — "Resource Acquisition Is Initialisation" — is the most important idea in C++. A resource (memory, a file, a lock, a network connection) is owned by an object: the constructor acquires it and the destructor releases it. Because destructors always run when objects go out of scope, cleanup happens automatically — even when errors occur.

The idea

C++
#include <iostream>
#include <stdexcept>
#include <string>

class Connection {
public:
    explicit Connection(std::string host) : host_(std::move(host)) {
        std::cout << "open " << host_ << '\n';
    }
    ~Connection() {
        std::cout << "close " << host_ << '\n';
    }
    void send(const std::string& message) {
        if (message.empty()) throw std::runtime_error("empty message");
        std::cout << "send \"" << message << "\" to " << host_ << '\n';
    }
private:
    std::string host_;
};

void talk() {
    Connection a{"db.example.com"};
    Connection b{"cache.example.com"};
    a.send("hello");
    b.send("");                 // throws
    a.send("never sent");
}

int main() {
    try {
        talk();
    } catch (const std::exception& e) {
        std::cout << "error: " << e.what() << '\n';
    }
}
Output
open db.example.com
open cache.example.com
send "hello" to db.example.com
close cache.example.com
close db.example.com
error: empty message

When the exception leaves talk, both connections are closed — in reverse order of opening — before the catch runs. No finally, no clean-up code to forget.

RAII in the standard library

You've been using RAII all along:

ResourceRAII type
Heap memory for many elementsstd::vector, std::string, std::map…
A single heap objectstd::unique_ptr, std::shared_ptr
An open filestd::ifstream, std::ofstream (lesson 44)
A mutex lockstd::lock_guard, std::scoped_lock (lesson 47)
A threadstd::jthread (C++20; joins in its destructor)
C++
#include <fstream>
#include <iostream>
#include <string>

int main() {
    {
        std::ofstream out{"greeting.txt"};    // file opened
        out << "Hello from RAII\n";
    }                                         // file flushed and closed here
    std::ifstream in{"greeting.txt"};
    std::string line;
    std::getline(in, line);
    std::cout << line << '\n';
}
Output
Hello from RAII

Wrapping a C resource

C libraries hand out resources that must be released with a specific function. A small RAII class — or a unique_ptr with a custom deleter — makes them safe:

C++
#include <cstdio>
#include <iostream>
#include <memory>

struct FileCloser {
    void operator()(std::FILE* f) const {
        if (f) {
            std::fclose(f);
            std::cout << "fclose called\n";
        }
    }
};

using FilePtr = std::unique_ptr<std::FILE, FileCloser>;

int main() {
    FilePtr f{std::fopen("log.txt", "w")};
    if (!f) return 1;
    std::fputs("written through a C API\n", f.get());
    std::cout << "leaving main\n";
}
Output
leaving main
fclose called

Scope guards

For one-off clean-up actions, a tiny generic "run this on scope exit" helper does the job:

C++
#include <iostream>
#include <utility>

template <typename F>
class ScopeExit {
public:
    explicit ScopeExit(F f) : f_(std::move(f)) {}
    ~ScopeExit() { f_(); }
    ScopeExit(const ScopeExit&) = delete;
    ScopeExit& operator=(const ScopeExit&) = delete;
private:
    F f_;
};

int main() {
    std::cout << "start task\n";
    ScopeExit done{[] { std::cout << "task finished (always printed)\n"; }};
    std::cout << "working...\n";
}
Output
start task
working...
task finished (always printed)

The rule of zero

If every member of your class manages itself (strings, vectors, smart pointers), the class needs no destructor, copy or move operations of its own — the compiler-generated ones do the right thing. This "rule of zero" is the goal for most classes. Only classes that directly manage a raw resource need to write those functions (lesson 35).

Try it yourself

Write a Timer class that prints a label when it's created and again when it's destroyed, and use it to mark the start and end of a block.

Try it yourself · main.cpp
Edit the code, copy it into main.cpp, then compile and run it with c++ -std=c++20 -Wall main.cpp -o main && ./main. How to set up a C++ compiler
Output
[start] main
[start] iteration 1
  doing work 1
[end]   iteration 1
[start] iteration 2
  doing work 2
[end]   iteration 2
[end]   main