Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 7 – Classes, objects and RAII

Destructors and RAII in C++

Write C++ destructors that release files, locks and other resources on every path out of a scope, and predict exactly when each object is destroyed.

  • Intermediate
  • 15 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Write destructors that release resources
  • Apply RAII to files, locks and timers
  • Predict the order in which objects are destroyed
  • Explain why a destructor must not let an exception escape

Before you start

What you’ll learn · Before you start

What you will learn

  • Write destructors that release resources
  • Apply RAII to files, locks and timers
  • Predict the order in which objects are destroyed
  • Explain why a destructor must not let an exception escape

Before you start

On this page

A function with three ways out, its normal end, an early return and an exception, needs three fclose calls to close one file, and the exception path skips even those. A destructor: The member function written ~T(), with no parameters and no return type, that runs when an object's lifetime ends: at the closing brace of its block, on a return, during stack unwinding, or after main for a global., written ~FileHandle(), removes the problem: it runs automatically when an object’s lifetime ends, at the closing brace of its block, on a return and while an exception passes through, so its code runs on every one of those paths. RAII: Resource acquisition is initialisation: a class acquires a resource, such as an open file or a locked mutex, in its constructor and releases it in its destructor, so every path out of the object's scope releases it exactly once., short for “resource acquisition is initialisation”, builds on that guarantee: a class acquires a resource, such as an open file or a locked mutex, in its constructor and releases it in its destructor, and the compiler inserts the release on every path itself.

This lesson prints the exact moment each destructor runs, with Apple clang 21 and GCC 16, wraps a C file handle and a lock in RAII classes, and shows the two ways a program can bypass destructors.

Destructors run in reverse order of construction

Noisy prints a + when an object is created and a - when it is destroyed. The program below creates objects in a block, as members of a class, as a temporary and as a global:

Lifetimes of locals, members, a temporary and a global

C++ · lifetimes.cpp

// When does each destructor run? Every Noisy object prints its own birth and death.
#include <print>
#include "noisy.h"

class Room {
public:
    ~Room() { std::println("  ~Room() body runs"); }   // members are destroyed after this

private:
    Noisy door_{"door"};      // constructed first, destroyed last
    Noisy window_{"window"};
};

Noisy global{"global"};       // static storage: lives until after main returns

int main() {
    std::println("main starts");
    Noisy a{"a"};
    {
        Noisy b{"b"};
        Noisy c{"c"};
        std::println("  the inner block ends");
    }
    Room room;
    Noisy{"temporary"};       // a temporary dies at the end of its statement
    std::println("main ends");
}

C header · noisy.h

// A type that prints when it is created (+) and when it is destroyed (-).
#pragma once
#include <print>

class Noisy {
public:
    explicit Noisy(const char* name) : name_{name} { std::println("  + {}", name_); }
    ~Noisy() { std::println("  - {}", name_); }

private:
    const char* name_;
};
The same order with both compilers Python · lifetimes_session.py
from session import run

# lifetimes.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra lifetimes.cpp -o lifetimes && ./lifetimes
g++-16 -std=c++23 -Wall -Wextra lifetimes.cpp -o lifetimes-gcc && ./lifetimes-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra lifetimes.cpp -o lifetimes && ./lifetimes
  + global
main starts
  + a
  + b
  + c
  the inner block ends
  - c
  - b
  + door
  + window
  + temporary
  - temporary
main ends
  ~Room() body runs
  - window
  - door
  - a
  - global
$ g++-16 -std=c++23 -Wall -Wextra lifetimes.cpp -o lifetimes-gcc && ./lifetimes-gcc
  + global
main starts
  + a
  + b
  + c
  the inner block ends
  - c
  - b
  + door
  + window
  + temporary
  - temporary
main ends
  ~Room() body runs
  - window
  - door
  - a
  - global

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lifetimes_session.py

Step through the run and watch each object appear and go, in the order the output printed them:

Interactive · step by step Every object of lifetimes.cpp, from + to - Step through the run. Each box is one object; its line says when it was made or destroyed, in the order the output printed.
Every object of lifetimes.cpp, from + to -, step 9 of 9. Showing global (destroyed 7th, the last), a (destroyed 6th), b (destroyed 2nd), c (destroyed 1st), room (~Room() body runs), room.door_ (destroyed 5th), room.window_ (destroyed 4th), Noisy{"temporary"} (made 7th, destroyed 3rd). In focus: global.globaldestroyed 7th, the lastadestroyed 6thbdestroyed 2ndcdestroyed 1stroom~Room() body runsroom.door_destroyed 5throom.window_destroyed 4thNoisy{"temporary"}made 7th, destroyed 3rd

The drawing shows the whole scene; its 9 steps are listed below.

Every step as text
  1. Before main. + global prints before main starts: an object with static storage duration is constructed before main runs.
  2. Three locals. main starts, then a, b and c are constructed in the order of their declarations: + a, + b, + c.
  3. The inner block ends. At the inner block's closing brace its two locals are destroyed in reverse order: - c, then - b. c was made after b, so it goes first.
  4. A Room and its members. Room room; constructs its members in declaration order: + door, then + window.
  5. A temporary. Noisy{"temporary"}; makes a temporary that dies at the end of its own statement: + temporary, - temporary, both before main ends is printed.
  6. The Room goes first. At the closing brace of main, room is destroyed first, because it was made after a, the other local still alive. Its destructor body runs while both members still exist: ~Room() body runs.
  7. Then its members. Then the members are destroyed in reverse declaration order: - window, then - door, the reverse of the order they were made in.
  8. Then a. a, the first local of main, is the last of its locals to go: - a.
  9. After main. global is destroyed after main returns: the first object made is the last one destroyed, - global.

Each line of the output follows one rule of the standard:

  • Locals die in reverse order of their construction. c was created after b, so c is destroyed first, as soon as the inner block’s closing brace is reached. a lives until the end of main.
  • A class’s destructor body runs first, then its members are destroyed in reverse declaration order. ~Room() prints before window, which goes before door, the reverse of the order they were created in.
  • A temporary dies at the end of the full expression that created it. Noisy{"temporary"}; is created and destroyed within its own statement, before main ends is printed.
  • Objects with static storage duration, such as globals, die after main returns, in the reverse order of their construction. global is the first object created and the last destroyed.

Reverse order is not an accident. An object constructed later may use the ones constructed before it, as window could use door, so destroying them backwards means every object is gone before the objects it depends on.

Arrays and containers

A built-in array destroys its elements in reverse order too: the standard says so. A standard container only promises that it destroys every element, without fixing an order, and the two standard libraries on this Mac really differ:

containers_order.cpp C++ · containers_order.cpp
// A built-in array destroys its elements in reverse order; the standard does not say
// in which order a std::vector destroys them.
#include <print>
#include <vector>
#include "noisy.h"

int main() {
    {
        std::println("array:");
        Noisy row[3] = {Noisy{"r0"}, Noisy{"r1"}, Noisy{"r2"}};
    }
    {
        std::println("vector:");
        std::vector<Noisy> list;
        list.reserve(3);
        list.emplace_back("v0");
        list.emplace_back("v1");
        list.emplace_back("v2");
    }
}
libc++ and libstdc++ destroy a vector in different orders Python · containers_order_session.py
from session import run

# The same program with libc++ (Apple clang) and libstdc++ (GCC 16).
run("""
clang++ -std=c++23 -Wall -Wextra containers_order.cpp -o containers_order && ./containers_order
g++-16 -std=c++23 -Wall -Wextra containers_order.cpp -o containers_order-gcc && ./containers_order-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra containers_order.cpp -o containers_order && ./containers_order
array:
  + r0
  + r1
  + r2
  - r2
  - r1
  - r0
vector:
  + v0
  + v1
  + v2
  - v2
  - v1
  - v0
$ g++-16 -std=c++23 -Wall -Wextra containers_order.cpp -o containers_order-gcc && ./containers_order-gcc
array:
  + r0
  + r1
  + r2
  - r2
  - r1
  - r0
vector:
  + v0
  + v1
  + v2
  - v0
  - v1
  - v2

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 containers_order_session.py

With Apple clang’s libc++, the vector destroys v2 first; with GCC’s libstdc++, v0 first. Both are correct. Code whose elements must be destroyed in a particular order has to arrange it, for example by popping them from the back one by one; it cannot rely on the vector’s destructor.

Checkpoint

Check: When and in which order destructors run

  1. Question 1 of 2 In lifetimes.cpp the inner block declares Noisy b{"b"}; and then Noisy c{"c"};. What does its closing brace print?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: - c, then - b

    An object made later may use the ones made before it, so destroying them backwards means every object is gone before the objects it depends on. The session printed - c, then - b.

  2. Question 2 of 2 A std::vector held v0, v1 and v2, added in that order. In which order did GCC's libstdc++ destroy them?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: v0, v1, v2, front to back

    The array printed - r2, - r1, - r0 with both libraries; the vector printed - v2 first with libc++ and - v0 first with libstdc++. Code that needs an order must arrange it itself.

RAII releases a resource on every path out of a scope

The C library hands out files as std::FILE* pointers from std::fopen, and every one of them must be passed to std::fclose exactly once. A function with three ways out needs three fclose calls, and an exception skips all of them. Wrapped in a class, the call is written once, in the destructor:

file_handle.cpp C++ · file_handle.cpp
// RAII: the constructor opens the file and the destructor closes it, on every path
// out of the function: the normal end, an early return and an exception.
#include <cstdio>
#include <format>
#include <print>
#include <stdexcept>
#include <string>

class FileHandle {
public:
    FileHandle(const std::string& path, const char* mode)
        : path_{path}, file_{std::fopen(path.c_str(), mode)} {
        if (file_ == nullptr) throw std::runtime_error{std::format("cannot open {}", path_)};
        std::println("  opened {}", path_);
    }

    ~FileHandle() {
        std::fclose(file_);
        std::println("  closed {}", path_);
    }

    FileHandle(const FileHandle&) = delete;              // one owner per file: copying is
    FileHandle& operator=(const FileHandle&) = delete;   // forbidden (next lesson: why)

    std::FILE* get() const { return file_; }

private:
    std::string path_;
    std::FILE* file_;
};

bool save_marks(const std::string& path, int marks) {
    FileHandle out{path, "w"};
    std::println(out.get(), "marks: {}", marks);
    if (marks < 0) {
        std::println("  negative marks, returning early");
        return false;                                    // ~FileHandle still runs
    }
    if (marks > 100) throw std::invalid_argument{"marks above 100"};   // and here too
    std::println(out.get(), "checked");
    return true;
}

int main() {
    std::println("save 72:");
    save_marks("marks.txt", 72);
    std::println("save -5:");
    save_marks("marks.txt", -5);
    std::println("save 140:");
    try {
        save_marks("marks.txt", 140);
    } catch (const std::invalid_argument& error) {
        std::println("  caught: {}", error.what());
    }
    std::println("read a missing file:");
    try {
        FileHandle in{"missing.txt", "r"};
    } catch (const std::runtime_error& error) {
        std::println("  caught: {}", error.what());   // no "closed": it was never opened
    }
}
Closed on the normal path, the early return and the exception Python · file_handle_session.py
from session import run

# file_handle.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra file_handle.cpp -o file_handle && ./file_handle
g++-16 -std=c++23 -Wall -Wextra file_handle.cpp -o file_handle-gcc && ./file_handle-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra file_handle.cpp -o file_handle && ./file_handle
save 72:
  opened marks.txt
  closed marks.txt
save -5:
  opened marks.txt
  negative marks, returning early
  closed marks.txt
save 140:
  opened marks.txt
  closed marks.txt
  caught: marks above 100
read a missing file:
  caught: cannot open missing.txt
$ g++-16 -std=c++23 -Wall -Wextra file_handle.cpp -o file_handle-gcc && ./file_handle-gcc
save 72:
  opened marks.txt
  closed marks.txt
save -5:
  opened marks.txt
  negative marks, returning early
  closed marks.txt
save 140:
  opened marks.txt
  closed marks.txt
  caught: marks above 100
read a missing file:
  caught: cannot open missing.txt

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 file_handle_session.py

The constructor acquires a resource; normal end, early return and exception all reach the destructor, which releases it.Constructoracquires the resource(opens the file)The object is in useinside its scopeThe scope is left:at the closing brace,by a return,or by an exceptionDestructorreleases the resource(closes the file)std::exit or std::abort:local destructorsdo not run

Every way out of a scope runs the destructor

Text description of the diagram

The diagram shows the life of an RAII object such as FileHandle.

  1. The constructor acquires the resource, for example by opening a file.
  2. The object is then in use inside its scope.
  3. The scope can be left in three ways: at the closing brace, by a return statement, or by an exception that unwinds the stack.
  4. Each of them leads to the destructor, which releases the resource, for example by closing the file.
  5. A dashed arrow from the object in use leads to a separate box: calling std::exit or std::abort ends the program without running the destructors of local objects, so the resource is not released by them.

The diagram shows the paths; the figure below follows the program along each of them, one FileHandle at a time:

Interactive · message by message One FileHandle, four ways out Step through the four parts of file_handle.cpp. The right column is the FileHandle; its line says whether its file is open.
One FileHandle, four ways out, step 5 of 5. Messages so far: main to save_marks: save_marks(…, 72); save_marks to main: returns true; save_marks to main: returns false; save_marks to main: invalid_argument; main to a FileHandle: FileHandle in{"missing.txt"}.maincatches the errorssave_markswrites one marka FileHandlenever existedsave_marks(…, 72)returns truereturns falseinvalid_argumentFileHandle in{"missing.txt"}

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Save 72: opened. main calls save_marks with 72. Its first line constructs out, whose constructor opens marks.txt and prints opened marks.txt.
  2. The normal end. 72 passes both checks, so the function reaches its closing brace. out's lifetime ends there: ~FileHandle closes the file and prints closed marks.txt.
  3. Save -5: an early return. With -5 the function prints negative marks, returning early and returns false from inside the if. The return ends out's lifetime too, so closed marks.txt is printed before main receives false.
  4. Save 140: an exception. 140 makes the function throw std::invalid_argument. Before the catch block in main receives it, stack unwinding destroys out: closed marks.txt, then caught: marks above 100.
  5. A constructor that fails. main constructs a FileHandle for missing.txt. Opening fails, so the constructor throws before it finishes. That object never existed: no destructor runs and nothing prints closed.

For 72 the function runs to its end. For -5 it returns early, and for 140 it throws an exception that main catches. In all three runs closed marks.txt is printed before control is back in main: the destructor ran on the way out. For the exception, this is stack unwinding: What happens while a thrown exception travels to its catch block: every local object constructed since the try block began, and not yet destroyed, is destroyed in reverse order before the handler runs.: before the catch block receives the exception, every local object that was constructed since the try block began is destroyed, in reverse order.

The last part shows the other half of the guarantee. Opening missing.txt fails, so the constructor throws, and an object whose constructor did not finish never existed: its destructor does not run, and nothing is printed as closed. Members that were already constructed, here path_, are destroyed. That is why a constructor acquires the resource last, or throws before it holds anything that needs releasing.

The two = delete lines forbid copying a FileHandle. A copy would hold the same std::FILE*, and both objects would close it. The next lesson explains these special member functions and the rules for writing them.

Key idea

Tie every resource to an object whose destructor releases it: the compiler then runs the release on every path out of the scope, the exception path included.

Checkpoint

Check: RAII on every path out of a scope

  1. Question 1 of 2 save_marks("marks.txt", 140) throws std::invalid_argument after out opened the file. When is closed marks.txt printed?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Before main's catch block prints caught, during stack unwinding

    The session printed opened marks.txt, closed marks.txt, caught: marks above 100, in that order. The destructor runs on the way out, whatever the way.

  2. Question 2 of 2 FileHandle in{"missing.txt", "r"}; throws from its constructor because the file cannot be opened. Does ~FileHandle run for in?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: No. The object never existed, but its member path_, already built, is destroyed

    An object whose constructor throws never existed, so its destructor does not run. That is why a constructor acquires its resource last, or throws before it holds anything that needs releasing.

The standard library already wraps most resources

The standard library already has an RAII class for files: std::ofstream and std::ifstream close their file in their destructors, which is why the exercise below can write a file in three lines without a close call. Most of the standard library manages a resource this way, and recognising the pattern tells you who releases what:

Standard RAII types (all in std) and what their destructors release
Criterion ResourceThe destructor
vector, string Memory for the elementsDestroys them, frees it
unique_ptr One object from newDeletes it
ofstream, ifstream An open fileCloses it
lock_guard, scoped_lock A locked mutexUnlocks it
jthread A running threadAsks it to stop, joins it

std::lock_guard needs nothing but a type with lock() and unlock() functions. PrintingLock below is such a type, which makes the guard’s work visible without starting any threads:

guard.cpp C++ · guard.cpp
// std::lock_guard locks in its constructor and unlocks in its destructor. Any type
// with lock() and unlock() works, so this one prints instead of blocking threads.
#include <mutex>
#include <print>
#include <stdexcept>

class PrintingLock {
public:
    void lock() { std::println("  lock()"); }
    void unlock() { std::println("  unlock()"); }
};

PrintingLock accounts_lock;
int balance = 100;

void withdraw(int amount) {
    std::lock_guard guard{accounts_lock};
    if (amount > balance) throw std::runtime_error{"not enough money"};
    balance -= amount;
    std::println("  balance is now {}", balance);
}

int main() {
    std::println("withdraw 30:");
    withdraw(30);
    std::println("withdraw 500:");
    try {
        withdraw(500);
    } catch (const std::runtime_error& error) {
        std::println("  caught: {}", error.what());
    }
}
Unlocked on the normal path and on the exception Python · guard_session.py
from session import run

# guard.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra guard.cpp -o guard && ./guard
g++-16 -std=c++23 -Wall -Wextra guard.cpp -o guard-gcc && ./guard-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra guard.cpp -o guard && ./guard
withdraw 30:
  lock()
  balance is now 70
  unlock()
withdraw 500:
  lock()
  unlock()
  caught: not enough money
$ g++-16 -std=c++23 -Wall -Wextra guard.cpp -o guard-gcc && ./guard-gcc
withdraw 30:
  lock()
  balance is now 70
  unlock()
withdraw 500:
  lock()
  unlock()
  caught: not enough money

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 guard_session.py

unlock() runs on both paths, including the one where withdraw throws before it changes the balance. With a real std::mutex, a missed unlock would make the next thread that asks for the lock wait forever, which is why rule R.1 of the C++ Core Guidelines asks for resource handles and RAII instead of paired calls written by hand. For a resource the table does not cover, such as a handle from a C library, write one small class like FileHandle for it, and let every other class hold that class as a member.

Checkpoint

Check: The standard library's RAII types

  1. Question 1 of 2 withdraw(500) locks through std::lock_guard and then throws, because 500 is more than the balance. What did guard.cpp print between lock() and caught?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: unlock()

    std::lock_guard locks in its constructor and unlocks in its destructor, so every path out of withdraw unlocks, the exception included.

  2. Question 2 of 2 A function writes a report file and has four return statements. Which standard type closes the file on every one of them without a close call?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: std::ofstream, whose destructor closes its file

    Prefer the standard RAII types: std::ofstream and std::ifstream close their files, std::vector frees its memory, std::lock_guard unlocks its mutex. Write your own class only for a resource they do not cover.

A destructor must not throw

A destructor that fails has nobody to report to: it may be running during stack unwinding for another exception, and its caller is usually the closing brace of a block. So the language treats a destructor as noexcept: A specifier that promises a function lets no exception out, as in Buffer(Buffer&&) noexcept. If one tries to leave such a function, std::terminate ends the program. A destructor is noexcept unless it says otherwise. unless it, or the destructor of one of its bases or members, says otherwise, and an exception that tries to leave it calls std::terminate, which ends the program:

throwing_destructor.cpp C++ · throwing_destructor.cpp
// A destructor is noexcept unless it says otherwise, so an exception that leaves it
// ends the program: the catch block below never runs.
#include <print>
#include <stdexcept>

class Report {
public:
    ~Report() {
        throw std::runtime_error{"could not save the report"};
    }
};

int main() {
    try {
        Report r;
    } catch (const std::exception& error) {
        std::println("caught: {}", error.what());
    }
    std::println("main ends");
}
Warnings at compile time, std::terminate at run time Python · throwing_destructor_session.py
from session import run

# Both compilers warn, and both programs stop in std::terminate. An exit status of
# 134 is 128 + 6: the program was stopped by signal 6, SIGABRT, which std::abort raises.
run("""
clang++ -std=c++23 -Wall -Wextra throwing_destructor.cpp -o throwing_destructor
./throwing_destructor
g++-16 -std=c++23 -Wall -Wextra throwing_destructor.cpp -o throwing_destructor-gcc
./throwing_destructor-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra throwing_destructor.cpp -o throwing_destructor
throwing_destructor.cpp:9:9: warning: '~Report' has a non-throwing exception specification but can still throw [-Wexceptions]
    9 |         throw std::runtime_error{"could not save the report"};
      |         ^
throwing_destructor.cpp:8:5: note: destructor has a implicit non-throwing exception specification
    8 |     ~Report() {
      |     ^
1 warning generated.
$ ./throwing_destructor
libc++abi: terminating due to uncaught exception of type std::runtime_error: could not save the report
[exit status 134]
$ g++-16 -std=c++23 -Wall -Wextra throwing_destructor.cpp -o throwing_destructor-gcc
throwing_destructor.cpp: In destructor 'Report::~Report()':
throwing_destructor.cpp:9:9: warning: 'throw' will always call 'terminate' [-Wterminate]
    9 |         throw std::runtime_error{"could not save the report"};
      |         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
throwing_destructor.cpp:9:9: note: in C++11 destructors default to 'noexcept'
$ ./throwing_destructor-gcc
terminate called after throwing an instance of 'std::runtime_error'
  what():  could not save the report
[exit status 134]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 throwing_destructor_session.py

Step through what happens between the closing brace of the try block and the end of the program:

Interactive · stage by stage An exception that tries to leave ~Report() Step through what happens when r goes out of scope, top to bottom. The catch block is right there, and it never runs.
An exception that tries to leave ~Report(), step 5 of 5. Showing The try block ends, ~Report() runs, noexcept by default, std::terminate, std::abort. In focus: std::abort.The try block endsr's lifetime ends~Report() runsit throws runtime_errornoexcept by defaultno catch can be reachedstd::terminatethe library's own messagestd::abortsignal 6, status 134

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Report r goes away. Report r; was the only object of the try block. At its closing brace the object's lifetime ends, and its destructor runs.
  2. The destructor throws. ~Report() throws std::runtime_error with the text could not save the report. Both compilers had warned: Clang with -Wexceptions, GCC with -Wterminate.
  3. noexcept by default. A destructor that says nothing about exceptions is noexcept, so the exception may not leave it. The catch block in main, one line away, is never reached.
  4. std::terminate. An exception that reaches the end of a noexcept function calls std::terminate. Each standard library printed its own message: libc++abi: terminating due to uncaught exception, and GCC's terminate called after throwing.
  5. The program is stopped. std::terminate calls std::abort, which raises SIGABRT, signal 6. The shell reports 128 + 6 = 134, the exit status the session shows, and main ends is never printed.

Both compilers see it coming. Clang’s -Wexceptions and GCC’s -Wterminate are both on by default and say that the throw can only end in terminate. At run time neither program reaches the catch block or prints main ends: each standard library prints its own message about the uncaught exception and aborts the program, and the exit status of 134 is how a shell reports signal 6, SIGABRT: 128 + 6.

Reporting a failed release from the destructor

~Report() { throw std::runtime_error{"could not save the report"}; } can only end the program. Rules C.36 and C.37 of the Core Guidelines say that a destructor must not fail and should be noexcept. When releasing a resource can fail, as flushing a file to a full disk can, give the class an explicit function such as close() that reports the error to a caller who can handle it, and let the destructor release whatever is still open while ignoring or logging failures.

std::ofstream works exactly like that: its close() sets an error state you can test, and the file buffer inside it, when destroyed, catches any exception from closing and does not rethrow it.

Checkpoint

Check: Destructors that do not throw

  1. Question 1 of 2 When ~Report() threw inside the try block, what happened?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: std::terminate ended the program, and the catch block never ran

    An exception that reaches the end of a noexcept function calls std::terminate. Each standard library printed its own message about the uncaught exception before the program was stopped.

  2. Question 2 of 2 std::terminate stopped the program through SIGABRT, signal 6, and a shell reports 128 plus the signal's number. Which exit status did the session show?

    Type a number.

    A hint

    Add the signal's number to 128.

    Show the answer: Question 2 of 2

    Answer: 134

    128 + 6 = 134, the status shown for both programs. In a terminal, echo $? after the program prints the same number.

Two ways to end a program skip destructors

Destructors run because the compiler places their calls at the end of each scope and on the unwinding path. A function that never returns to those places skips them:

exit_skips.cpp C++ · exit_skips.cpp
// std::exit ends the program without destroying local objects.
#include <cstdlib>
#include "noisy.h"

Noisy global{"global"};

int main() {
    Noisy local{"local"};
    std::exit(0);   // global is destroyed; local never is
}
global is destroyed; local is not Python · exit_skips_session.py
from session import run

# exit_skips.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra exit_skips.cpp -o exit_skips && ./exit_skips
g++-16 -std=c++23 -Wall -Wextra exit_skips.cpp -o exit_skips-gcc && ./exit_skips-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra exit_skips.cpp -o exit_skips && ./exit_skips
  + global
  + local
  - global
$ g++-16 -std=c++23 -Wall -Wextra exit_skips.cpp -o exit_skips-gcc && ./exit_skips-gcc
  + global
  + local
  - global

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exit_skips_session.py

std::exit destroys objects with static storage duration, such as global, but the standard says that objects with automatic storage duration are not destroyed by it, so local never prints its -. std::abort, which std::terminate calls by default, runs no destructors at all:

Which destructors run, by how the program ends
Criterion Static objectsLocal objects
main returns Yes, after mainYes, at each closing brace
std::exit(0) YesNo
std::abort() NoNo

Both exits are fine for a program that is about to stop anyway, since the operating system reclaims its memory and closes its files. They are not fine when a destructor has work that matters, such as writing the last buffered lines of a log. Returning from main runs every destructor.

Checkpoint

Check: Exits that skip destructors

  1. Question 1 of 2 exit_skips.cpp printed + global, + local and - global. Why is there no - local?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: std::exit destroys objects with static storage duration, but not local ones

    std::exit returns to none of the places where the compiler put the destructor calls of local objects, so those never run. Globals are destroyed by std::exit itself.

  2. Question 2 of 2 Which change to exit_skips.cpp makes - local appear?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Return from main instead of calling std::exit(0)

    Returning from main runs every destructor: main's locals at its closing brace, then the objects with static storage duration.

Practise: a timer and a temporary file

scope_tools.h declares two RAII classes whose destructors are missing:

  • ScopedTimer remembers when it was created and, when its scope ends, prints a line such as load marks: 25 ms to the stream it was given.
  • TempFile creates a file holding some text and should delete the file when it is destroyed.
The starter code and its sample tests

C header · scope_tools.h

// Starter code for "Your turn": give ScopedTimer and TempFile their destructors.
#pragma once
#include <chrono>
#include <filesystem>
#include <fstream>
#include <ostream>
#include <print>
#include <string>

// Reports how long a scope took, when the scope ends: "load marks: 12 ms".
class ScopedTimer {
public:
    ScopedTimer(const std::string& label, std::ostream& out)
        : label_{label}, out_{out}, start_{std::chrono::steady_clock::now()} {}

    // TODO: a destructor that prints "<label>: <n> ms" and a newline to out_, where n is
    // the whole number of milliseconds since start_:
    //     auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
    //         std::chrono::steady_clock::now() - start_).count();

    ScopedTimer(const ScopedTimer&) = delete;
    ScopedTimer& operator=(const ScopedTimer&) = delete;

private:
    std::string label_;
    std::ostream& out_;
    std::chrono::steady_clock::time_point start_;
};

// Creates a file holding the given text, and deletes the file when it is destroyed.
class TempFile {
public:
    TempFile(const std::string& path, const std::string& text) : path_{path} {
        std::ofstream file{path_};
        file << text;
    }   // the ofstream closes the file here, in its own destructor

    // TODO: a destructor that deletes the file: std::filesystem::remove(path_);

    TempFile(const TempFile&) = delete;
    TempFile& operator=(const TempFile&) = delete;

    const std::string& path() const { return path_; }

private:
    std::string path_;
};

C++ · scope_tools_tests.cpp

// Sample tests for scope_tools.h. Build: clang++ -std=c++23 -Wall -Wextra scope_tools_tests.cpp -o scope_tools_tests
#include <filesystem>
#include <fstream>
#include <iterator>
#include <print>
#include <sstream>
#include <stdexcept>
#include <string>
#include <thread>
#include "scope_tools.h"

int passed = 0;
int failed = 0;

void expect(bool ok, const char* what) {
    if (ok) {
        ++passed;
    } else {
        ++failed;
        std::println("FAIL {}", what);
    }
}

// "label: 25 ms\n" -> 25; -1 when the text does not have that shape.
long long reported_ms(const std::string& text, const std::string& label) {
    std::string start = label + ": ";
    std::string end = " ms\n";
    if (!text.starts_with(start) || !text.ends_with(end)) return -1;
    std::string digits = text.substr(start.size(), text.size() - start.size() - end.size());
    if (digits.empty() || digits.find_first_not_of("0123456789") != std::string::npos) return -1;
    return std::stoll(digits);
}

std::string read_file(const std::string& path) {
    std::ifstream in{path};
    return {std::istreambuf_iterator<char>{in}, std::istreambuf_iterator<char>{}};
}

int main() {
    // Files left behind by an earlier run of these tests would make them pass by accident.
    std::filesystem::remove("scratch.txt");
    std::filesystem::remove("unwind.txt");

    std::ostringstream log;
    {
        ScopedTimer timer{"load marks", log};
        std::this_thread::sleep_for(std::chrono::milliseconds{25});
        expect(log.str().empty(), "the timer prints nothing while its scope runs");
    }
    expect(reported_ms(log.str(), "load marks") >= 25, "the timer reports at least 25 ms when its scope ends");

    std::ostringstream nested;
    {
        ScopedTimer outer{"outer", nested};
        {
            ScopedTimer inner{"inner", nested};
        }
    }
    expect(nested.str().starts_with("inner: ") && nested.str().find("\nouter: ") != std::string::npos,
           "the inner timer reports before the outer one");

    {
        TempFile temp{"scratch.txt", "roll 12: 91.5\n"};
        expect(std::filesystem::exists("scratch.txt"), "the file exists while the TempFile lives");
        expect(read_file(temp.path()) == "roll 12: 91.5\n", "the file holds the text");
    }
    expect(!std::filesystem::exists("scratch.txt"), "the file is gone after its scope");

    try {
        TempFile temp{"unwind.txt", "x"};
        throw std::runtime_error{"something failed"};
    } catch (const std::runtime_error&) {
    }
    expect(!std::filesystem::exists("unwind.txt"), "the file is gone after an exception, too");

    std::println("{} of {} checks passed", passed, passed + failed);
    return failed == 0 ? 0 : 1;
}
The sample tests against the starter code Python · scope_tools_session.py
from session import run

# The sample tests against the starter code, before the TODOs are done.
run("""
clang++ -std=c++23 -Wall -Wextra scope_tools_tests.cpp -o scope_tools_tests && ./scope_tools_tests
g++-16 -std=c++23 -Wall -Wextra scope_tools_tests.cpp -o scope_tools_tests-gcc && ./scope_tools_tests-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra scope_tools_tests.cpp -o scope_tools_tests && ./scope_tools_tests
In file included from scope_tools_tests.cpp:10:
./scope_tools.h:26:19: warning: private field 'out_' is not used [-Wunused-private-field]
   26 |     std::ostream& out_;
      |                   ^
1 warning generated.
FAIL the timer reports at least 25 ms when its scope ends
FAIL the inner timer reports before the outer one
FAIL the file is gone after its scope
FAIL the file is gone after an exception, too
3 of 7 checks passed
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra scope_tools_tests.cpp -o scope_tools_tests-gcc && ./scope_tools_tests-gcc
FAIL the timer reports at least 25 ms when its scope ends
FAIL the inner timer reports before the outer one
FAIL the file is gone after its scope
FAIL the file is gone after an exception, too
3 of 7 checks passed
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 scope_tools_session.py

Clang notices the problem before the tests do: out_ is never used, because no destructor writes to it. Write the two destructors until both compilers print 7 of 7 checks passed:

  1. ~ScopedTimer() computes the milliseconds as the comment shows and prints label_, a colon, the number and ms on one line: std::println(out_, "{}: {} ms", label_, ms);.
  2. ~TempFile() calls std::filesystem::remove(path_);. Note what the last test proves: the file is removed even when an exception leaves the block.

Then check the order rule from the start of the lesson: the test with two nested timers passes only because the inner timer, created last, is destroyed first.

Now try the ideas on code you have not seen yet, from the order of the minus signs to the two exits. The session returns first to whichever idea your answers got wrong most, and explains each choice as you make it:

Practice that adapts to you

Practise this lesson

10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 In which order are the three objects destroyed when the function returns?

    Read the code, then choose one answer.

    void f() {
        Noisy first{"first"};
        Noisy second{"second"};
        Noisy third{"third"};
    }
    Show the answer: Question 1 of 10

    Answer: third, second, first

    Local objects are destroyed in reverse order of their construction, so an object created later, which may use the earlier ones, is always destroyed first.

  2. Question 2 of 10 A Room object with the members door_ and window_, declared in that order, is destroyed. Put the steps in order.

    Give each item its position, from 1 (first).

    Show the answer: Question 2 of 10

    Answer:

    1. The body of ~Room() runs
    2. window_ is destroyed
    3. door_ is destroyed

    The destructor's body runs first, while every member still exists. Then the members are destroyed in reverse declaration order.

  3. Question 3 of 10 A function creates a FileHandle, then returns early from inside an if. What happens to the file?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: The FileHandle's destructor closes it as the function returns

    A return ends the lifetime of every local object of the function, so their destructors run on the way out. That is the point of RAII: the release is tied to the scope, not to a line of code.

  4. Question 4 of 10 A constructor throws an exception before it finishes. Does the object's destructor run?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: No. The object never existed, but members already constructed are destroyed

    A destructor runs only for an object whose construction completed. The members and base classes that were already constructed are destroyed during the unwinding, so a resource held by an RAII member is still released.

  5. Question 5 of 10 What does the destructor of a std::vector release?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: It destroys the elements and frees the memory they were in

    Most of the standard library manages a resource this way, which tells you who releases what: a vector its memory, a stream its file, a guard its lock.

  6. Question 6 of 10 What does std::lock_guard need from the type it guards?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: Two member functions, lock() and unlock()

    std::lock_guard calls lock() in its constructor and unlock() in its destructor. With a real std::mutex, a missed unlock would make the next thread that asks for the lock wait forever.

  7. Question 7 of 10 What happens when an exception leaves a destructor that has no noexcept specification of its own?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: std::terminate is called and the program ends

    Destructors are noexcept unless they, or the destructor of a base or a member, say otherwise. An exception that reaches the end of a noexcept function calls std::terminate; both compilers warn about it by default.

  8. Question 8 of 10 Releasing a resource can fail, as flushing a file to a full disk can. How should a class report that failure?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: Through an explicit function such as close() that the caller can check, while the destructor ignores or logs failures

    Give the class a close() that reports the error, and let the destructor release whatever is still open without throwing. std::ofstream's close() sets an error state you can test.

  9. Question 9 of 10 Which destructors run when the program calls std::abort?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: None at all

    std::abort runs no destructors, and std::exit runs only those of objects with static storage duration. Both are fine for a program about to stop anyway, not when a destructor has work that matters.

  10. Question 10 of 10 Which destructors run when main calls std::exit(0)?

    Choose every answer that is right.

    Show the answer: Question 10 of 10

    Answer:

    • Those of global objects
    • Those of static local objects that were already constructed

    std::exit destroys objects with static storage duration, which includes globals and static locals, but the standard says objects with automatic storage duration are not destroyed by it. std::abort runs no destructors at all.

Interview questions

Fresher · What does RAII mean, and what does it guarantee?

RAII, resource acquisition is initialisation, means tying a resource to the lifetime of an object: the constructor acquires it and the destructor releases it. Because the destructor of a local object runs on every path out of its scope, at the closing brace, on a return and during stack unwinding when an exception passes through, the release cannot be forgotten and happens exactly once. The standard library is built this way: std::vector frees its memory, std::ofstream closes its file, std::lock_guard unlocks its mutex and std::unique_ptr deletes its object. A complete answer names the limits too: the guarantee covers objects whose constructor finished, it does not hold if the program ends through std::exit or std::abort, and the destructor must not throw.

The limits are where the follow-ups go: a constructor that throws after one of its members took a resource, and a log that is cut short by std::abort. Decide both:

Checkpoint

Interview round: two follow-up questions

  1. Question 1 of 2 Follow-up: a constructor opens a file into a FileHandle member, then throws from its body. Is the file closed?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Yes. The member's own constructor finished, so unwinding destroys it, and its destructor closes the file

    This is why members that manage themselves make constructors safe: whatever a constructor acquired through finished members is released even when its body throws.

  2. Question 2 of 2 Follow-up: a program writes its last lines to a std::ofstream log and then calls std::abort(). What can be lost?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Lines still in the stream's buffer, because the ofstream's destructor never runs to flush and close it

    The operating system reclaims memory and closes files when a program ends, but a destructor that has work that matters, such as writing the last buffered lines of a log, needs the program to end normally.

Locks and globals are where RAII bugs hide in real services, and the next two questions take one each: a lock guard that protects nothing, and a crash that comes after main returns:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid

    A review finds std::lock_guard<std::mutex>{mtx}; at the top of a function that updates shared data, yet a data race shows up under load. Explain why the lock protects nothing, give the fix, and say how you would stop this bug from coming back.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A service sometimes crashes after main returns. A global Cache in cache.cpp writes a summary through a global Logger from logger.cpp in its destructor, and the crash appeared after a change to the link order. Explain the cause and give two fixes, with their costs.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Destructors run where lifetimes end, in reverse order of construction: locals at their block’s closing brace, a temporary at the end of its statement, members after their class’s destructor body and globals after main.
  • RAII acquires a resource in a constructor and releases it in the destructor, so the normal end, an early return and stack unwinding all release it, while an object whose constructor threw never existed and is never destroyed.
  • The standard library already wraps memory, files, locks and threads, so a class of your own is needed only for a resource it does not cover.
  • Destructors are noexcept by default, so an exception that leaves one calls std::terminate, and release errors are reported from an explicit function such as close().
  • std::exit skips the destructors of local objects and std::abort skips all of them, while returning from main runs every one.

Terms in this lesson

destructor
The member function written ~T(), with no parameters and no return type, that runs when an object's lifetime ends: at the closing brace of its block, on a return, during stack unwinding, or after main for a global.
noexcept
A specifier that promises a function lets no exception out, as in Buffer(Buffer&&) noexcept. If one tries to leave such a function, std::terminate ends the program. A destructor is noexcept unless it says otherwise.
RAII
Resource acquisition is initialisation: a class acquires a resource, such as an open file or a locked mutex, in its constructor and releases it in its destructor, so every path out of the object's scope releases it exactly once.
stack unwinding
What happens while a thrown exception travels to its catch block: every local object constructed since the try block began, and not yet destroyed, is destroyed in reverse order before the handler runs.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.