Skip to content
academia.sh

Lesson 04 / 14

RAII

Measures that the member a guard acquires and releases is a part inside the guard object, not the guard itself.

Published Updated

In this lesson

  • Defines that RAII acquires a resource in the constructor and releases it in the destructor
  • Measures that the object a guard touches is not itself but the member inside it
  • Shows that the destructor runs even on early exit paths
Contents

The previous lesson measured when an object is constructed and destroyed, whether it has a name or not. This lesson puts that knowledge to a purpose: releasing a resource safely — here a simple counter-like resource, in real programs it could be memory, a file handle, or a lock. The measurement kind this time is part: the thing a guard acquires and releases is not the guard itself, but a data member inside it.

What RAII Is

RAII (Resource Acquisition Is Initialization) is a design pattern that has a resource acquired in an object’s constructor and released in its destructor. As long as the object holding the resource is alive, the resource stays valid; when the object — named or temporary, as seen in the previous lesson — reaches the end of its lifetime by any path, the destructor runs and releases the resource. This makes releasing the resource by hand (calling a function like release() one by one in every piece of calling code) unnecessary: no matter which path the object holding the resource is destroyed by (reaching the end of a function, an early return, throwing an exception), the destructor always runs.

RAII’s opposite is leaving the resource to the caller’s discipline instead of a constructor/destructor pair: an open_resource() function is called, used, and a close_resource() is expected to be called by hand at every exit point of the code. This approach’s fragility shows up as the code grows — when a new early return is added to a function, a close_resource() call must also be added before that return; if it is not added, the resource leaks (leak — the resource stays valid for the rest of the program’s run without ever being released). RAII removes this risk by handing the release decision not to the code holding the resource but to the responsibility of the type of the object holding it: code that uses Guard never writes a release call, because that call is already written once, in the class itself, inside Resource::~Resource(). This lesson’s first section defines two classes: Resource, which actually holds the resource, and Guard, which wraps it as a member.

// resource.hpp — a simple resource represented by numbers
#ifndef RESOURCE_HPP
#define RESOURCE_HPP

#include <cstdio>

class Resource {
public:
    explicit Resource(int id) : id_(id) {
        std::printf("resource acquired id=%d\n", id_);
    }

    ~Resource() {
        std::printf("resource released id=%d\n", id_);
    }

    int id() const { return id_; }

private:
    int id_;
};

#endif
// guard.hpp — a guard that acquires and releases the member holding the resource
#ifndef GUARD_HPP
#define GUARD_HPP

#include "resource.hpp"

class Guard {
public:
    explicit Guard(int id) : resource_(id) {}

    int id() const { return resource_.id(); }

private:
    Resource resource_;
};

#endif

What the Guard Releases Is a Part, Not the Guard Itself

Guard has its own constructor and a compiler-generated destructor; when Guard constructs its resource_ member, Resource’s constructor runs, and when Guard itself is destroyed, resource_ is destroyed and Resource’s destructor runs. The question here: is the thing the guard acquires and releases the Guard object itself, or the resource_ member inside it?

// part-measurement.cpp — is the resource the guard holds the guard itself or a part inside it
#include <cstdio>

#include "guard.hpp"

int main() {
    std::printf("block started\n");
    {
        Guard g(7);
        std::printf("id %d\n", g.id());
    }
    std::printf("block ended\n");
    return 0;
}
block started
resource acquired id=7
id 7
resource released id=7
block ended

CR8. Resource’s constructor is triggered on the Guard g(7) line, before Guard’s own constructor body runs, by resource_(id) in the member initializer list — this is why the resource acquired line is seen right after block started, before Guard’s own body prints anything. The object that actually acquires the resource is Resource, and this object is not Guard itself but a member of it — resource_ is a subobject inside Guard’s memory range, not a separate, independent object. The proof of this is in the ordering: when g is destroyed at the end of the block, Guard’s (compiler-generated, printing nothing) destructor runs first, and then, because it is a member of Guard, resource_’s destructor runs — this is why the resource released line is seen before block ended. If resource_ were an independent object separate from Guard, its destruction would not depend on Guard’s destruction; but here the two are triggered at the same moment, by the closing of the same scope — because one is a part of the other.

There is no name collision to worry about here: Resource and Guard are each their own class, one holding the other as a member. Guard‘s own constructor, explicit Guard(int id) : resource_(id) {}, has no body — the member initializer list does all the work, constructing resource_ with id. Guard has no destructor written of its own; the compiler generates one, and this generated destructor, printing nothing itself, merely triggers resource_‘s destructor — this is the same behavior seen in the previous lesson’s Pair example: even when a class writes no destructor of its own, its members’ destructors still run.

The Destructor Still Runs on Early Exit Paths

RAII’s real value is writing the code that releases the resource in a single place (the destructor) and leaving it to the compiler to run that code on every exit path — a function’s normal end, an early return, or even when an exception is thrown.

// early-exit.cpp — does the guard still release on an early return path
#include <cstdio>

#include "guard.hpp"

static void run(bool early_exit) {
    Guard g(9);
    std::printf("run started id=%d\n", g.id());
    if (early_exit) {
        std::printf("exiting early\n");
        return;
    }
    std::printf("finishing normally\n");
}

int main() {
    run(true);
    std::printf("main continues\n");
    return 0;
}
resource acquired id=9
run started id=9
exiting early
resource released id=9
main continues

CR9. When run(true) reaches the return inside the if block, g’s scope (the function body) ends, and the compiler runs g’s destructor right before the return — this is why the resource released line is seen after exiting early, but before main continues. This behavior is the same regardless of the if branch: had run(false) been called, the same destructor would still run right after the finishing normally line. The code that releases the resource is written only once, inside Resource::~Resource(); run itself makes no release call at all.

This guarantee holds not only for regular control flow like if/return, but also for C++’s exception mechanism: if an exception is thrown (throw) inside a function body, the stack unwinds (stack unwinding) until that exception is caught by a catch block — during this unwinding, the destructors of the local objects in each scope being unwound run in order, exactly as with a normal return. Had there been a throw line after g inside run, g‘s destructor would still run before the exception left run. This course does not teach throwing and catching exceptions — that is another topic — but RAII’s real strength comes from exactly this: the code that releases the resource does not need to know which path control flow exits by, because the destructor runs on every path.

The Risk of Holding a Resource by Reference

RAII’s soundness rests on the guard holding the resource by value (as a member, bound to its own lifetime). If a guard holds the resource by reference or pointer, the guard’s own lifetime and the resource’s real owner’s lifetime can diverge — the resource may already be gone while the guard is still alive. Guard does not carry this risk because resource_ is not a reference or a pointer but a direct Resource value; as long as Guard is alive, resource_ is alive too, because it is a part of the same object. Had a guard been wrongly designed to hold the resource by pointer instead of by value — something like Resource* resource_; — copying the guard would copy only the pointer, not the actual Resource object; two guards would point at the same resource, and once one released the resource in its destructor, the other would end up pointing at an invalid resource. This lesson does not run this mistake — it is only mentioned in prose — because releasing a resource twice is undefined behavior.

Solving this sharing problem with the standard library’s ready-made RAII wrappers is K05’s subject; this course manages the resource with classes it writes itself. M10/K02’s (Rust) ownership system and M11/K02’s resource management are that same idea’s — that a resource has exactly one owner, and the resource is released automatically once the owner’s lifetime ends — counterparts in different languages; this lesson does not deepen that comparison.

It is useful to see in more detail how Guard‘s own design avoids this risk. resource_ is a Resource value, not a Resource* or a Resource&; this is why resource_ itself (not merely an address pointing at it) sits in the Guard object’s memory layout. When a Guard is copied — this course’s next lesson’s subject — resource_ is copied too, because it too is a part of Guard; two Guards carry two separate Resources, not sharing the same resource. This is not true for a resource held by pointer: copying the pointer copies only the address, not the pointed-to object, so two guards begin pointing at the same resource. This consequence of holding by value — that copying also copies the resource along with it — is a part of RAII’s reliability and is measured in detail in the next lesson.

Holding a resource by value has a limit too: because the resource sits in the holding object’s memory layout, the resource’s size (or at least the size of the handle representing it) must be known at compile time. This problem does not show up here because Resource carries only an int; a real resource — an open file, a network connection — is usually represented by a fixed-size handle given by the operating system (for example an integer or a pointer), which is why the same hold-by-value strategy works for real resources too.

A Guard Holding More Than One Resource

A guard holding more than one resource instead of a single one follows the same rule: each resource becomes another member of the guard, and each member’s construction/destruction order follows the declaration-order rule measured in the previous lesson. Had a DoubleGuard with two Resource members been designed, the members would be constructed in declaration order and destroyed in reverse order — exactly like first_/second_ in the Pair example. This lesson does not run DoubleGuard, because acquiring and releasing a single resource is enough to show RAII’s core idea; but it is important to see that this generalization comes directly from the previous lesson — RAII adds no new language rule, it merely puts the constructor/destructor order to a purpose.

The Bounding Measurement

The expression this lesson measures is of the part kind: resource_ is not Guard itself but a subobject inside it, and it has the same lifetime as Guard. But RAII’s guarantee depends on the guard holding the resource by value — if a guard holds only a reference to the resource (the resource’s real owner being some other object), the guard must not outlive the protected object; if the protected object is destroyed while the guard is still alive, the guard’s reference becomes dangling, and the guard’s destructor (if it has one) may try to access an invalid object. This lesson does not run this risk, because running it would trigger undefined behavior; Guard here always holds the resource by value, so this risk does not exist to begin with.

A practical consequence of this bound is that when designing a guard, the question “who really owns this resource” must always be answered explicitly. In Guard the answer is clear: Guard itself owns it, because resource_ is a value member of it. Had a guard instead wanted to say “I am only observing a resource living elsewhere” — for example wanting to store a Resource& taken as a function parameter — this would no longer be RAII, only holding a reference, and the responsibility of releasing the resource would not belong to the guard. Both designs are valid in C++, but they must not be confused with each other: a class’s destructor must release only the resources it itself owns, not one it has merely borrowed.

The three examples measured throughout this lesson — the proof of construction order, the destructor running on early exit, holding the resource by value — together show why RAII is more than a “best practice”: it is a direct consequence of the language’s constructor/destructor and declaration-order rules, not a separate mechanism. The next lesson’s copying question is also built on this same foundation — when a Guard is copied, resource_ will be copied too, but whether this copy is “deep” or “shallow” can produce different results depending on the resource’s type.

Summary

  • The expression this lesson measures is of the part kind: resource_ is not the Guard object itself, but a subobject inside it.
  • RAII acquires a resource in the constructor and releases it in the destructor; no code that releases the resource by hand is needed.
  • The destructor runs regardless of how the scope ends — a normal end, an early return, or throwing an exception, it is the same in all of them.
  • A guard that holds the resource by value binds the guard’s lifetime to the resource’s lifetime; a guard that holds it by reference loses this guarantee.
  • Smart pointers extend RAII to shared resources; this is outside this course’s scope (K05).

Next Step

This lesson measured how a resource is safely held by a constructor/destructor pair; the next lesson asks what this guarantee becomes when an object is copied — it measures whether the copy constructor acquires its own resource, or shares the original’s.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close