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.
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:
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_;
}; 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:
The drawing shows the whole scene; its 9 steps are listed below.
Every step as text
- Before main. + global prints before main starts: an object with static storage duration is constructed before main runs.
- Three locals. main starts, then a, b and c are constructed in the order of their declarations: + a, + b, + c.
- 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.
- A Room and its members. Room room; constructs its members in declaration order: + door, then + window.
- A temporary. Noisy{"temporary"}; makes a temporary that dies at the end of its own statement: + temporary, - temporary, both before main ends is printed.
- 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.
- Then its members. Then the members are destroyed in reverse declaration order: - window, then - door, the reverse of the order they were made in.
- Then a. a, the first local of main, is the last of its locals to go: - a.
- 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.
cwas created afterb, socis destroyed first, as soon as the inner block’s closing brace is reached.alives until the end ofmain. - A class’s destructor body runs first, then its members are destroyed in reverse declaration order.
~Room()prints beforewindow, which goes beforedoor, 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, beforemain endsis printed. - Objects with static storage duration, such as globals, die after
mainreturns, in the reverse order of their construction.globalis 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:
// 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");
}
} 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
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:
// 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
}
} 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
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.
- The constructor acquires the resource, for example by opening a file.
- The object is then in use inside its scope.
- The scope can be left in three ways: at the closing brace, by a return statement, or by an exception that unwinds the stack.
- Each of them leads to the destructor, which releases the resource, for example by closing the file.
- 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:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Save 72: opened. main calls save_marks with 72. Its first line constructs out, whose constructor opens marks.txt and prints opened marks.txt.
- 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.
- 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.
- 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.
- 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
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:
| Criterion | Resource | The destructor |
|---|---|---|
| vector, string | Memory for the elements | Destroys them, frees it |
| unique_ptr | One object from new | Deletes it |
| ofstream, ifstream | An open file | Closes it |
| lock_guard, scoped_lock | A locked mutex | Unlocks it |
| jthread | A running thread | Asks 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:
// 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());
}
} 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
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:
// 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");
} 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:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- 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
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:
// 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
} 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:
| Criterion | Static objects | Local objects |
|---|---|---|
| main returns | Yes, after main | Yes, at each closing brace |
| std::exit(0) | Yes | No |
| std::abort() | No | No |
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
Practise: a timer and a temporary file
scope_tools.h declares two RAII classes whose destructors are missing:
ScopedTimerremembers when it was created and, when its scope ends, prints a line such asload marks: 25 msto the stream it was given.TempFilecreates a file holding some text and should delete the file when it is destroyed.
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;
} 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:
~ScopedTimer()computes the milliseconds as the comment shows and printslabel_, a colon, the number andmson one line:std::println(out_, "{}: {} ms", label_, ms);.~TempFile()callsstd::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.
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
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.
-
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.
-
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.
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
returnand 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
noexceptby default, so an exception that leaves one callsstd::terminate, and release errors are reported from an explicit function such asclose(). std::exitskips the destructors of local objects andstd::abortskips all of them, while returning frommainruns 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
- Working Draft, Programming Languages: C++ (N4950): [class.dtor], [except.ctor], [except.spec], [except.terminate], [support.start.term], [filebuf.cons], [ofstream.members] (ISO/IEC JTC1/SC22/WG21)
- C++ Core Guidelines: R.1, manage resources automatically using resource handles and RAII (Standard C++ Foundation)
- C++ Core Guidelines: C.31, all resources acquired by a class must be released by the class destructor (Standard C++ Foundation)
- C++ Core Guidelines: C.36, a destructor must not fail (Standard C++ Foundation)
- C++ Core Guidelines: C.37, make destructors noexcept (Standard C++ Foundation)
- Diagnostic flags in Clang (-Wexceptions) (LLVM Project)
- Options Controlling C++ Dialect (-Wno-terminate) (GNU Project)
- Bash Reference Manual: Exit Status (GNU Project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress