all_lessons/C++/05 · RAIIlesson 6 / 20

RAII: the central idea of C++

Lesson 04 ended on a defeat: with raw new/delete, freeing memory correctly is unwinnable by discipline alone. A single early return, or an exception thrown between the new and the matching delete, silently leaks — and the more error paths a function grows, the more places you must remember to free. The one new idea in this lesson dissolves that whole problem: tie a resource's lifetime to the lifetime of a local object, so that cleanup is not something you remember to do but something the language does for you, deterministically, on every exit path. This is RAII, and it is the idea that organizes everything that follows.

The thesis, here
The cost-model lens, applied to cleanup: in a garbage-collected language you cannot say when a resource is released — that decision is hidden inside a collector you don't control. C++ refuses to hide it, and RAII is how it makes the answer to "when does it die?" not just visible but exact: at the closing brace of the enclosing scope, guaranteed, every time. Determinism is the feature; RAII is the mechanism that buys it at zero runtime overhead.
Linear position
Prerequisite: Lesson 04 — you have seen the heap, raw new/delete, and the three failure modes (leak, dangling pointer / use-after-free, double free), and you have seen that an early return or a thrown exception between new and delete leaks. You should also recall from Lesson 02 that automatic (stack) objects are destroyed at scope exit, and from Lesson 03 what a pointer is.
New capability: design a type whose constructor acquires a resource and whose destructor releases it, so cleanup runs automatically and deterministically at scope exit — including on early return and on a thrown exception — and explain why this is the one discipline no garbage-collected language can teach you.
The plan
Five moves. (1) Recall the unwinnable leak from Lesson 04 and name what is missing: a guaranteed cleanup hook. (2) Introduce the destructor and the one rule that makes RAII work — a stack object's destructor cannot be skipped. (3) Build a worked FileHandle that owns a file and watch it survive the exact early-return and exception paths that leaked in Lesson 04. (4) Contrast RAII against the two alternatives the rest of the world uses — garbage collection and try/finally — and show why both are weaker. (5) Generalize: any resource, not just memory, and a second worked example, ScopedTimer.

1 · The hole Lesson 04 left

Here is the shape of the problem from Lesson 04, made concrete. A function opens a file, does some work, and must close it before returning. With the raw C-style API, "close it" is a statement you write, and you must write it on every path out of the function:

// Lesson-04 style: manual cleanup, written by hand on every exit path.
void process(const char* path) {
    std::FILE* f = std::fopen(path, "r");   // ACQUIRE the resource
    if (!f) return;                          // (nothing to free yet — fine)

    char line[256];
    if (!std::fgets(line, sizeof line, f)) {
        return;                  // BUG: early return — we never fclose(f). LEAK.
    }
    if (line[0] == '#') {
        throw std::runtime_error("comment");  // BUG: throw — fclose(f) skipped. LEAK.
    }
    // ... real work ...
    std::fclose(f);              // the ONLY path that frees it
}

There are three exits from this function and only one of them frees the file. The leak is not a typo — it is the structural fact that cleanup is decoupled from the resource. Every new return, every break, every exception that can propagate through is one more place the author must remember to insert an fclose. As Lesson 04 argued, this does not scale: a function with five error paths needs the cleanup repeated five times, and the sixth path someone adds next year will forget. Discipline does not compose.

What is missing is a hook the language guarantees to run — a single place to write "close the file" that fires no matter how control leaves the function. C++ has exactly such a hook, and it is attached not to the function but to an object.

2 · The destructor, and the one rule that makes RAII work

Recall from Lesson 02 that a local variable has automatic storage duration: it lives on the stack, and it is destroyed automatically when the scope it was declared in ends. C++ lets a class define exactly what "destroyed" means by giving it a destructor — a special member function named ~ClassName(), taking no arguments, that the compiler calls automatically at the end of the object's lifetime. Its mirror image is the constructor, the function that runs when the object is created. The whole technique is just this pairing:

Constructor acquires
When the object is created, its constructor takes ownership of a resource — opens the file, allocates the memory, locks the mutex, starts the timer. After the constructor finishes, "I exist" means "I hold the resource."
Destructor releases
When the object's lifetime ends, its destructor gives the resource back — closes the file, frees the memory, unlocks the mutex. The cleanup code is written once, in the type, not at every call site.
The guarantee
For a stack object, the destructor runs at scope exit unconditionally — on normal fall-through, on return, on break, and crucially on a thrown exception passing through. You cannot forget it; you cannot skip it.

The name RAII stands for Resource Acquisition Is Initialization: you acquire the resource in the act of initializing an object, which binds the resource's lifetime to that object's scope. The name is famously bad — the load-bearing half is the release in the destructor, not the acquisition — but the idea is precise and it is the most important one in the language.

The reason the destructor cannot be skipped on an exception is stack unwinding. When a throw happens, control does not simply jump to the handler; the runtime walks back up the call stack frame by frame, and as it leaves each scope it runs the destructor of every fully-constructed automatic object in that scope, in reverse order of construction, before moving to the next frame up. That is the mechanism. (Lesson 15 builds the entire treatment of exception safety on top of this; for now just hold the fact: unwinding runs destructors, so an RAII object cleans up even while an exception is tearing the stack down.)

3 · Worked example: a FileHandle that cannot leak

Now rewrite the leaky process with a tiny class that owns the file. The constructor opens; the destructor closes. Nothing else changes about the logic.

class FileHandle {
    std::FILE* f_;
public:
    explicit FileHandle(const char* path, const char* mode)
        : f_(std::fopen(path, mode)) {}     // ACQUIRE in the constructor

    ~FileHandle() {                          // RELEASE in the destructor
        if (f_) std::fclose(f_);             // runs at scope exit, always
    }

    bool ok()  const { return f_ != nullptr; }
    std::FILE* get() const { return f_; }

    // We OWN the file, so forbid copying for now — a copy would mean two
    // handles closing the same FILE* (a double free). Lesson 06 fixes this
    // properly with move semantics; Lesson 07 makes ownership a type.
    FileHandle(const FileHandle&)            = delete;
    FileHandle& operator=(const FileHandle&) = delete;
};

void process(const char* path) {
    FileHandle file(path, "r");          // constructor opens the file
    if (!file.ok()) return;              // early return — ~FileHandle() still runs

    char line[256];
    if (!std::fgets(line, sizeof line, file.get())) {
        return;                          // early return — ~FileHandle() runs → fclose
    }
    if (line[0] == '#') {
        throw std::runtime_error("comment");  // throw — unwinding runs ~FileHandle()
    }
    // ... real work ...
}                                        // normal exit — ~FileHandle() runs → fclose
Worked trace — the file closes on all four exits
Walk the four ways control can leave process and note that the destructor fires on every one:
exit path what runs at the closing scope ──────────────────────────── ────────────────────────────────── 1. fopen failed, return early ~FileHandle(): f_ is null → no-op (correct) 2. fgets failed, return early ~FileHandle(): fclose(f_) ✓ closed 3. throw std::runtime_error unwinding → ~FileHandle(): fclose(f_) ✓ closed 4. fall off the end normally ~FileHandle(): fclose(f_) ✓ closed
Compare this to §1, where only path 4 closed the file and paths 2 and 3 leaked. We did not add cleanup to the three new exits — we removed the cleanup statement entirely and let the destructor carry it. The function got shorter and stopped leaking at the same time. That is the payoff: cleanup is now structurally impossible to forget, because there is no longer a cleanup statement to forget.

Notice what made this work: file is a plain automatic (stack) object. We never wrote new or delete; the resource it owns happens to be a FILE*, but the handle lives on the stack and the stack's destruction rules do the rest. This is the modern-C++ stance the orientation promised: you almost never call delete by hand — you build a small owning object and let its destructor run.

4 · Why not garbage collection, and why not try/finally

If you come from Python, Java, JavaScript, Go, or C#, you have two reflexes for this problem, and it is worth seeing precisely why C++ chose neither.

ApproachWhen does cleanup run?What it coversThe catch
Garbage collection (Java, Go, Python, JS)Eventually — at some unspecified later time the collector chooses, possibly never before exitMemory onlyNon-deterministic and memory-only: the GC reclaims heap memory when it gets around to it, but it does not promptly close your file, release your lock, or flush your socket. Those are not memory, and the collector neither knows nor cares about them.
try/finally (Java, Python, JS)At the end of the try block, deterministicallyAnything you write in finallyManual and repeated: you write the cleanup, at every site that acquires, and you must remember the finally each time. Same "discipline doesn't compose" failure as §1, just with a guaranteed-to-run block.
RAII (C++)Deterministically at scope exit, on every path including exceptionsAny resource — memory, files, locks, sockets, timers, transactionsYou write the cleanup once, in the destructor, when you design the type. Every use site gets it for free.

The GC point is the sharp one. A garbage collector solves memory leaks — and only at a time it chooses. But a file handle, a database connection, a held mutex, a GPU buffer: these are scarce resources where when you release matters as much as whether. Holding a file open "until the GC runs" can mean holding it for seconds, or until the program exits — which is why GC languages had to bolt on a second mechanism (try/finally, with in Python, try-with-resources in Java, defer in Go, using in C#) precisely because the collector cannot do deterministic, prompt, non-memory cleanup. RAII needs no second mechanism: the same scope rule that frees memory frees everything.

The idea no GC language teaches you
This is worth stating flatly. If you have only ever used garbage-collected languages, RAII is a concept you have never been forced to learn, because the collector hid the "when does it die?" question for memory and a finally/defer block papered over it for everything else. C++ makes object lifetime the tool for managing every resource, which is why a C++ programmer's instinct — "wrap the resource in a type whose destructor releases it" — transfers back to Go's defer, Python's context managers, and Rust's ownership model (a direct, enforced descendant of RAII). Learning it once rewires how you think about cleanup in every language.

5 · Generalize: any resource, and a second worked example

Memory is just one resource. The acquire-in-constructor / release-in-destructor pattern applies to anything with a "set up" and a "tear down" that must be paired: a mutex (lock / unlock — Lesson 17 makes RAII locks the only sane way to hold one), a database transaction (begin / commit-or-rollback), a buffer borrowed from a pool, even something as abstract as "the duration of a code block." Here is a ScopedTimer that measures how long its scope took — the cleanup is not freeing anything, it is reporting, and it must run on every exit just the same:

class ScopedTimer {
    const char* name_;
    std::chrono::steady_clock::time_point start_;
public:
    explicit ScopedTimer(const char* name)
        : name_(name), start_(std::chrono::steady_clock::now()) {}  // ACQUIRE: start clock

    ~ScopedTimer() {                                                 // RELEASE: report
        auto end = std::chrono::steady_clock::now();
        auto us  = std::chrono::duration_cast<std::chrono::microseconds>(end - start_).count();
        std::printf("%s took %lld us\n", name_, static_cast<long long>(us));
    }
};

double risky_compute(const std::vector<double>& data) {
    ScopedTimer t("risky_compute");   // clock starts here
    if (data.empty())
        return 0.0;                   // timer still reports on this early return
    if (data[0] < 0)
        throw std::domain_error("neg"); // timer still reports while unwinding
    // ... expensive work ...
    return /* result */ 0.0;
}                                     // timer reports here on normal exit

You construct one local ScopedTimer at the top of any scope you want to measure and forget about it; the destructor prints the elapsed time when the scope ends, on every path, including the throw. No finally, no remembering to stop a stopwatch, no duplicated reporting code on each return. This is the same mechanism as FileHandle — the only difference is what "release" means. That generality is why RAII is called the idea of C++ rather than just a memory trick.

6 · Common mistakes / failure modes

Letting the destructor throw
If an exception is already unwinding the stack and a destructor throws a second exception, the program calls std::terminate and dies. Destructors must not throw — swallow or log errors in cleanup, never propagate them. (Lesson 15 returns to this.)
Acquiring on the heap and forgetting the handle
auto* fh = new FileHandle(...); defeats the whole point — now the handle itself leaks unless you delete it, and you are back to Lesson 04. Keep the RAII object on the stack (automatic storage) so its destructor is guaranteed.
Copying an owning object
If FileHandle allowed copies, two handles would hold the same FILE* and both destructors would fclose it — a double free (Lesson 04). We = deleted copying as a stopgap; Lesson 06 (move) and Lesson 07 (smart pointers) solve it properly.
Doing real work in the constructor body, not the init list
If the constructor acquires a resource and then throws after acquiring (in its body), the destructor will not run — the object was never fully constructed. Acquire in the member initializer list, or guard partial construction. (A subtlety, but the reason the standard library uses RAII building blocks rather than hand-rolled ones.)

Checkpoint exercise

Try it
Take the leaky §1 process and the §3 FileHandle version. (1) Compile both with -fsanitize=address (AddressSanitizer) — or run them under valgrind — feeding a file whose first line starts with # so the throw path fires, and confirm the raw version reports a leaked FILE* while the RAII version reports none. (2) Add a std::printf("opening\n") in the constructor and std::printf("closing\n") in the destructor of FileHandle, then trigger each of the four exit paths from the §3 trace and confirm "closing" prints exactly once every time. (3) As a thought experiment: write what the equivalent ScopedTimer would look like in Python using a try/finally, and count how many lines you must repeat at each call site versus the C++ version's zero.

Where this points next

RAII says "the owning object's destructor frees the resource." That immediately raises a question we dodged with = delete: what happens when you copy or pass such an object? If two FileHandles end up owning the same file, both destructors fire and you have the double free from Lesson 04 back again. Lesson 06 — copy, move, and value semantics — answers it: it defines what copying an owning object should mean (deep copy vs. transfer), introduces move semantics so ownership can be handed off cheaply instead of duplicated, and gives you the rule of zero/three/five for deciding which special functions a resource-owning class needs. RAII is the what; copy and move are the how a value carrying a resource flows through your program.

Takeaway
RAII — Resource Acquisition Is Initialization — is the central idea of C++: bind a resource's lifetime to a local object's scope, acquire it in the constructor and release it in the destructor, and the language guarantees the destructor runs at scope exit on every path — normal fall-through, early return, and a thrown exception, because stack unwinding runs destructors (the hook Lesson 15 builds exception safety on). This makes cleanup deterministic and automatic instead of a discipline you must repeat at every exit — directly curing the leak failure mode from Lesson 04 — and it applies to any resource (files, locks, sockets, timers), not just memory. It is strictly stronger than garbage collection (non-deterministic and memory-only, so GC languages still bolt on finally/defer/with for prompt non-memory cleanup) and than try/finally (manual and repeated at every call site). It is the discipline no garbage-collected language forces you to learn — and once you have, you see it in Go's defer, Python's context managers, and Rust's ownership everywhere.

Interview prompts