Skip to content
VLSI Mentor

C++ Part I · Module 12 · Constructors and Destructors

RAII: Tying a Resource to an Object's Lifetime

Three modules have been working toward this chapter. Module 10 established that manual release does not survive ordinary edits, measured three leaks appearing in a correct function from three unremarkable changes, and ended by naming the missing idea as ownership — which code is responsible for releasing this, and when — while admitting it could not yet deliver an answer, because answering required an object able to make a promise and keep it. Module 11 built that object: a type that owns an invariant, with access control making the invariant enforceable. This module supplied the three guarantees that make the promise keepable, and they are worth restating because the technique is nothing more than their consequence. A constructor runs on every path that creates an object and cannot be skipped. A destructor runs on every path that destroys one, including paths created by exceptions, and including the partial case where construction never finished. And members are torn down in the exact reverse of their construction, automatically. Put an acquisition in the first and a release in the second, and the release happens wherever the acquisition did — not because anyone remembered, but because the language has no path on which it does not. This chapter measures that against the manual alternative on the same eight acquisitions.

Intermediate19 min readC++Part IFundamentalsRAII

Part I · Module 12 · Chapter 12.9 · RAII: Tying a Resource to an Object's Lifetime

1. The Idiom

Acquire the resource in the constructor. Release it in the destructor.

That is the whole technique. The name is RAII — Resource Acquisition Is Initialization — which names the first half and leaves the important half implicit, since the release is where the benefit comes from.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    class Handle {
    public:
        explicit Handle(const char* what) { /* acquire */ }
        ~Handle()                         { /* release */ }

        Handle(const Handle&)            = delete;
        Handle& operator=(const Handle&) = delete;
    };

Chapter 10.8 §7 showed this shape and deferred the explanation. Everything needed to explain it is now in place.

2. Why It Works

Three measured guarantees, from the three chapters before this one:

A constructor cannot be skipped. There is no path from "this object exists" to "its constructor did not run". Chapter 11.1 §6 measured the enforcement — Field h; would not compile because the type had no valid default.

A destructor runs on every path. Chapter 12.7 §6's table: automatic objects at the closing brace, on every path, including those created by exceptions — which Chapter 10.8 §8 measured running innermost-first before the catch body.

And destruction is the exact reverse of construction (Chapter 12.8), over whatever was actually built — Chapter 12.7 §8 measured a throw during the second of three members destroying only the first.

RAII is those three facts used on purpose. Nothing more.

3. The Shape of Construction and Destruction

The figure gathers the seven steps the last eight chapters measured, because the idiom sits in exactly two of them.

Seven steps of an object's existence, read top to bottom. Step one: storage is obtained, raw bytes with no object yet. Step two: members are constructed, in declaration order, before the constructor body. Step three: the constructor body runs, which is where a resource is acquired and the invariant is checked. Step four: the object is alive and its lifetime has begun. Step five: the destructor body runs, which is where the resource is released. Step six: members are destroyed in the exact reverse of construction. Step seven: storage is released. The language guarantees steps one, two, four, six and seven, so an acquisition in step three is matched by a release in step five on every exit path including an exception.01STORAGE IS OBTAINEDraw bytes, no object yet02MEMBERS ARE CONSTRUCTEDdeclaration order, before the body03THE CONSTRUCTOR BODY RUNSacquire here; check the invariant04THE OBJECT IS ALIVEits lifetime has begun05THE DESTRUCTOR BODY RUNSrelease here, on every path06MEMBERS ARE DESTROYEDexact reverse of construction07STORAGE IS RELEASEDthe bytes go back
Figure 1 — the seven steps of an object's existence, as measured across Chapters 12.1, 12.7 and 12.8. RAII occupies steps 3 and 5: the constructor body acquires and the destructor body releases. The language performs steps 1, 2, 4, 6 and 7 with a guaranteed order and no way to skip them, which is what turns an acquisition in step 3 into a release in step 5 on every exit path. Read downward; the numbers give the direction.

Steps 3 and 5 are yours. The other five are the language's, and they are the reason the technique is a guarantee rather than a convention.

(Many standard types already are step 3 and 5 for you: std::string acquires and releases its characters — Chapter 9.5 measured the move from the stack region to the heap — and std::fstream closes its file in its destructor, which is Chapter 23.4's subject.)

4. The Measurement

A Handle whose constructor acquires and destructor releases, with a live counter so the state is visible. Five exit paths, eight acquisitions.

Azvya Education Pvt. Ltd.VLSI Mentor
raii12.cpp — the type
static int liveResources = 0;
static int acquireCount  = 0;
static int releaseCount  = 0;

class Handle {
public:
    explicit Handle(const char* what) : what_(what)
    {
        ++liveResources; ++acquireCount;
        std::printf("    acquire %-10s (live=%d)\n", what_, liveResources);
    }
    ~Handle()
    {
        --liveResources; ++releaseCount;
        std::printf("    release %-10s (live=%d)\n", what_, liveResources);
    }
    Handle(const Handle&)            = delete;
    Handle& operator=(const Handle&) = delete;
private:
    const char* what_;
};

static void normalExit()   { Handle h("normal"); }
static void earlyReturn(bool bail) { Handle h("early"); if (bail) return; /* ... */ }
static void throws()       { Handle h("throwing"); throw std::runtime_error("boom"); }
static void twoResources() { Handle a("outer"); Handle b("inner"); throw std::runtime_error("boom2"); }
static void loop()         { for (int i = 0; i < 3; ++i) Handle h("in-loop"); }

Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
normal exit:
    acquire normal     (live=1)
    release normal     (live=0)
early return:
    acquire early      (live=1)
    release early      (live=0)
exception:
    acquire throwing   (live=1)
    release throwing   (live=0)
    caught
two resources, throw after both:
    acquire outer      (live=1)
    acquire inner      (live=2)
    release inner      (live=1)
    release outer      (live=0)
    caught
loop, three iterations:
    acquire in-loop    (live=1)
    release in-loop    (live=0)
    acquire in-loop    (live=1)
    release in-loop    (live=0)
    acquire in-loop    (live=1)
    release in-loop    (live=0)

acquired 8   released 8   still live 0

Eight acquisitions, eight releases, nothing left live. And the live counter returns to zero after every situation — it never exceeds 2, and 2 only while two resources are deliberately held.

Note the third and fourth blocks particularly. In the exception case there is no try in throws() and no cleanup code of any kind; the release happened during unwinding. In the two-resource case inner was released before outer — Chapter 12.8's reversal, which is what makes nesting safe.

5. The Same Five Situations by Hand

Identical structure, explicit acquire/release calls instead of a type:

Azvya Education Pvt. Ltd.VLSI Mentor
manual12.cpp — two of the five
static void earlyReturn(bool bail)
{
    acquire("early");
    if (bail) return;                       // release never reached
    release("early");
}

static void twoResources()
{
    acquire("outer");
    acquire("inner");
    throw std::runtime_error("boom2");      // neither released
    release("inner"); release("outer");
}

Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
normal exit:
    acquire normal     (live=1)
    release normal     (live=0)
early return:
    acquire early      (live=1)
exception:
    acquire throwing   (live=2)
    caught
two resources, throw after both:
    acquire outer      (live=3)
    acquire inner      (live=4)
    caught
loop with a continue:
    acquire in-loop    (live=5)
    release in-loop    (live=4)
    acquire in-loop    (live=5)
    acquire in-loop    (live=6)
    release in-loop    (live=5)

acquired 8   released 3   STILL LIVE 5

6. The Comparison

acquiredreleasedstill live
RAII880
manual835

Same eight acquisitions. Five leaked.

And the shape of the two traces differs in a way worth noticing beyond the totals: the manual version's live counter climbs — 1, 2, 3, 4, 5, 6 — while the RAII version's returns to 0 after every situation. In a program that runs for minutes rather than milliseconds, that climb is Chapter 10.6's measured growth from 1,312 KB to 195,808 KB.

This is Chapter 10.8's result on a larger sample: there, three ordinary edits to a correct function produced 3 leaks, 96 bytes, zero compiler warnings.

7. What the Compiler Contributes: Nothing

This deserves its own section because the claim is stronger than "the standard flags miss it", and I checked it specifically.

FlagsWarnings about the leak
-Wall -Wextra -Wpedantic0
-Wunreachable-code2 — but about code will never be executed after a throw, not about the leak
-Weverything3 warnings, none about the leak
clang --analyzenothing

No flag at any level reports the skipped release of a custom resource. -Wunreachable-code catches the two release calls sitting after a throw, which is a genuine help and a different finding — it does not notice the early-return path, where the release is perfectly reachable and simply not reached on that branch. The static analyzer finds nothing because acquire and release are ordinary functions it knows nothing about.

(A correction to my own earlier working: a grep of mine reported -Weverything mentioning "leak" twice, and that was a false positive — it matched the identifier liveResources in my own source and an unrelated -Wmissing-noreturn warning. The three -Weverything warnings are about system include paths, an old-style cast, and a missing prototype.)

For memory specifically the tools are better — Chapter 10.6 measured leaks --atExit naming the exact source line. For a lock, a file handle or a vendor handle there is no such tool, which is Chapter 10.5's callout argument: the lifetime framing covers every resource and the sanitizers cover one.

8. Two Resources: Use Two Members

The §4 trace held two resources as two separate objects, which is the general technique:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    class Session {
    public:
        Session(const char* path, const char* lockName)
            : lock_(lockName), file_(path) {}      // acquire both, in order
        // no destructor at all
    private:
        Lock   lock_;        // declared first  -> released last
        File   file_;        // declared second -> released first
    };

Session has no destructor. Each member releases itself, and Chapter 12.8's reversal guarantees file_ goes before lock_ — so the file is closed while the lock is still held, which is almost always what you want.

And the case that makes this genuinely safe rather than merely tidy: if file_'s constructor throws, lock_ is released automatically. Chapter 12.7 §8 measured exactly that — a throw during the second member destroys the first and nothing else, and the enclosing object's destructor never runs because it never existed. No try/catch in the constructor, no partial-cleanup code.

Writing one destructor that releases both would be worse in three ways: it has to get the order right by hand, it does not run at all if construction failed halfway, and it needs its own error handling if the first release can fail.

One resource per class, and compose.

9. The Question This Chapter Does Not Answer

The = delete lines in §4 are not decoration, and Chapter 12.4 §7 explained the mechanism without settling the design.

A Handle that owns a resource and is copied gives two objects one resource and therefore two releases — Chapter 10.7's double delete, measured there as exit 133, SIGTRAP. Deleting the copy operations makes that unexpressible, which is correct and incomplete: it means a Handle cannot be returned from a factory, stored in a container, or passed by value.

So this chapter's type is honest and restricted. The full answer is Module 13: Chapter 13.4 is Shallow Copy, Deep Copy and the Double Free That Follows, Chapter 13.8 is move construction — which is how an owner is transferred rather than duplicated — and Chapter 13.11 is the Rule of Zero, the observation that a type composed of self-managing members needs none of this written by hand.

Deleting copies is right permanently when the resource represents a unique claim: a lock, a file handle, a scoped registration. It is a placeholder when the resource is merely data that happens to be dynamically allocated.

10. The Ones Already Written

Almost every RAII class you need exists:

ResourceUseChapter
a run of objectsstd::vector19.2
textstd::string9.5
one owned objectstd::unique_ptr14.2
a shared objectstd::shared_ptr14.5
a filestd::fstream23.4
a mutexstd::lock_guard / std::scoped_lockModule 26

So the honest guidance is: write an RAII class only for a resource the standard library does not cover — a vendor handle, a C API's opaque pointer, a registration that must be undone. That is a small, identifiable set, usually a handful of types in a large codebase, and Chapter 14.4's custom deleters often handle even those without a new class.

11. What to Actually Do

Acquire in the constructor, release in the destructor — for any resource, not only memory.

Use a standard type when one exists. §10's table covers nearly everything.

One resource per class, and compose — §8's two members rather than one two-resource destructor.

Declare members in the order you want them acquired, remembering release is the reverse.

Write no destructor for the composing type. Each member handles itself.

Delete the copy operations while the type owns a raw resource, and treat that as permanent only for a unique claim.

Expect no help from the compiler on the manual version — measured: nothing at any warning level.

And settle copy and move semantics before shipping a hand-written owner (Module 13).

12. Mistakes Worth Making Once

Releasing by hand and trusting the paths. Measured: 8 acquired, 3 released, 5 live.

Assuming a warning flag will catch it. Measured: nothing, at any level, including -Weverything and the static analyzer.

Thinking it is only about memory. For a lock there is no sanitizer — and the symptom is a deadlock elsewhere.

Writing one destructor for two resources. Wrong order by hand, and it does not run if construction failed halfway.

Declaring a lock after the data it protects. Reverse destruction then releases the lock first.

Copying a resource-owning object with default copy semantics. Chapter 10.7's double delete — exit 133.

Deleting copies and calling the design finished. Often Module 13's question deferred.

Writing an RAII class for something std::vector already does.

Letting the destructor throw. Chapter 12.7 §9 — it terminates during propagation.

13. Interview Relevance

"What is RAII?" Acquire in the constructor, release in the destructor. The answer that distinguishes is why it works: a destructor runs on every path out of a scope, including exception unwinding, so the release cannot be skipped by control flow added later.

"Why is it better than a cleanup block?" Because a cleanup block covers the paths its author thought of. The measured comparison — 8 acquired with 3 released by hand versus 8 released by a destructor — is the strongest form of this answer.

"Does it apply to anything besides memory?" Yes, and this is where it matters most: locks, files, handles, registrations — none of which any sanitizer checks.

"How do you handle two resources in one class?" Two members, each owning one, with no destructor on the enclosing class — and the order is the declaration order, reversed on destruction. Mentioning that a throw in the second member's constructor releases the first automatically is a strong detail.

"What is the catch?" Copying. A type with a destructor and default copy semantics is a double free waiting for its second owner, which is why the copy operations are deleted here and why Module 13 exists.

"Do you write RAII classes often?" The good answer is no — std::vector, std::string, std::unique_ptr and std::lock_guard cover nearly everything, and a hand-written one is for a resource the library does not model.

14. What You Should Be Able to Say Now

  1. Acquire in the constructor, release in the destructor — RAII names the first half.
  2. It works because of three measured guarantees: a constructor cannot be skipped, a destructor runs on every path including exception unwinding, and destruction is the exact reverse of construction over whatever was built.
  3. Measured, five exit paths, eight acquisitions: acquired 8 released 8 still live 0.
  4. The exception case had no try and no cleanup code — the release happened during unwinding.
  5. Measured, the same five by hand: acquired 8 released 3 STILL LIVE 5.
  6. The manual live counter climbs (1→6) where the RAII one returns to 0 every time.
  7. Measured: the compiler contributes nothing — 0 at the standing flags, and -Weverything plus the static analyzer found nothing about the leak.
  8. -Wunreachable-code catches a release after a throw — a different and narrower finding.
  9. For memory there are leak tools; for a lock or a handle there are none — which is where RAII earns most.
  10. Two resources: two members, no destructor — and a throw in the second member's constructor releases the first automatically (Chapter 12.7 §8).
  11. Declaration order is acquisition order; release is the reverse (Chapter 12.8).
  12. Copying is the unanswered question — deleted here, and Module 13's subject.
  13. Deleting copies is permanent for a unique claim, a placeholder for data.
  14. Most RAII classes are already written — vector, string, unique_ptr, fstream, lock_guard.
  15. The destination is the Rule of Zero (Chapter 13.11) — compose, and write none of it.

15. Exercises

Exercise 1 — write one. Implement a FileHandle wrapping a FILE* with RAII. Say which operations you delete and why.

Exercise 2 — predict. For the Handle type, give the trace for: a function that acquires one and returns early; one that acquires two and throws between them; one that acquires one in a loop of four iterations.

Exercise 3 — the comparison. Explain why the manual version released 3 of 8, naming the path that failed in each of the four failing situations.

Exercise 4 — compose it. A class needs a lock and a buffer. Give the member declarations in the right order, say what has no destructor, and state what happens if the buffer's constructor throws.

Exercise 5 — no tool will help. Explain why a leak checker would catch the memory version of this bug and not the lock version, and what that implies.

Exercise 6 — use the library. Replace a hand-written RAII class for each: a dynamic array of samples, an owned single object, a file, a mutex.

Exercise 7 — the copy problem. Show the two-line change that turns a correct Handle into a double free, and say which chapter fixes it properly.

Exercise 8 — permanent or placeholder? Classify the deleted copy operations as permanent or a stopgap: a lock guard, a buffer of samples, a scoped registration, a file handle, a reference-counted texture.

16. Summary

  • Acquire in the constructor, release in the destructor — and the release is the half that matters.
  • It works because a constructor cannot be skipped, a destructor runs on every path including unwinding, and destruction exactly reverses construction over what was built.
  • Measured: acquired 8 released 8 still live 0 across five exit paths, with no cleanup code anywhere.
  • Measured, by hand: acquired 8 released 3 STILL LIVE 5, with the live count climbing.
  • Measured: no compiler flag at any level reports it — including -Weverything and the static analyzer.
  • For locks, files and handles there is no sanitizer at all — RAII's strongest case.
  • Two resources: two members, no destructor, and a failed second construction releases the first.
  • Copying is the open question — deleted here, answered in Module 13.
  • Most of these classes already exist; the destination is the Rule of Zero.

The technique is in place and the module has one thing left to do with it. Every example here has been a function written to demonstrate a single point — one resource, one exit path, one situation at a time — and real code is not shaped like that. A function of thirty lines will contain locals of several storage durations, temporaries that exist for part of one statement, a reference bound to something whose lifetime has been extended, an object returned by value that may or may not be copied on the way out, and members of each of those with lifetimes of their own. The last chapter of the module takes one such function and accounts for every object in it, saying for each one when it dies and what that releases — which turns out to include a result that is easy to assume is an optimisation and is in fact guaranteed.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.