Module 13 — Worked Examples¶
Five programs in examples/module-13/, each folder with its own expected-output.txt. Build any of
them with the course's command:
Every program prints its own reasoning as it goes, so you can read the output as an argument rather than a dump of values. Each section below gives you something to predict before you scroll to the output — do that. The prediction is where the learning is, and several of these programs are designed so that the honest prediction is wrong.
One demonstration in this module is deliberately not a folder under examples/: the program that
calls std::terminate cannot exit normally, so the automated check that compares captured output
cannot verify it. It is quoted in §6, source and transcript together, and you are expected to run it
yourself.
1. The same failure, reported three ways¶
examples/module-13/01-error-codes-versus-exceptions/main.cpp
Predict first: the program calls one operation whose second call must fail. Which version — the status one or the throwing one — leaves the caller able to ignore the failure, and what does each version force every intermediate function to do?
The core of the program is one operation written twice:
// 1. a status enum, returned
BorrowStatus borrowByCode(const std::string& title);
// 2. an exception, thrown
void borrowByException(const std::string& title); // throws ItemUnavailable
Part A shows honest use of the first; Part B the second; Part C compares what each demands; Part D shows the standard library publishing both policies side by side; Part E measures the cost a status imposes on a three-frame call chain.
Part A - an error code, checked honestly
borrowByCode("Compilers") -> success
borrowByCode("Compilers") -> already on loan <- the checkout worked
borrowByCode("Unknown Title") -> no such item
Three results, three branches. This code is correct - because every
call site checks. `[[nodiscard]]` on the function asks the compiler to
warn when a caller does not, which is the strongest help a return value
can get, and it is only a warning.
Part C - what each forces on the caller
status: the caller MUST remember to read the result. Nothing stops
the value being discarded, and the program then behaves as
though the operation succeeded.
exception: the caller must handle it or let it propagate. There is no
third option - an unhandled exception ends the program.
What to notice. The status version is not a strawman: it is correct, it is fast, and it is the right design for some failures. What it cannot do is survive a careless caller. Part E names the real cost of the choosing wrongly in either direction:
What that costs is on the THROWING path, not the normal one: a call that
succeeds is not slower for being able to throw.
The trade is about expectation and frequency, never about speed alone.
2. What unwinding actually destroys¶
examples/module-13/02-unwinding-and-cleanup/main.cpp
Predict first: throwMidway() constructs three named locals and then throws. Predict the order of
the destruction lines, and predict how many; then predict whether a resource released by a statement
is released when the statement is skipped.
Part A - unwinding destroys locals, in reverse order
[member] constructed a
[member] constructed b
[member] constructed c
[member] destroyed c
[member] destroyed b
[member] destroyed a
[handler] caught: failed after three locals existed
Then the leak, counted rather than argued:
Part B - a raw resource, released by a statement
throwing path:
[device] acquired raw sensor (handle 2)
[handler] caught: raw sensor read failed
two calls, and the number of devices still live went from 0 to 1
What to notice. The release statement in Part B is correct — it is simply not reached, and by the time the handler runs the local handle is gone, so nothing can release it later. Part C writes the same resource as an RAII member and gets three releases out of three paths, including the throwing one:
Part C - the same resource, owned by an object
normal path: released raii sensor (handle 3)
early return: released raii sensor (handle 4)
a throw: released raii sensor (handle 5)
three paths, releases in this part: 3
Part D then delivers the case that catches almost everyone — a constructor that acquires a resource in its body and then throws:
Part D - a constructor that throws destroys only what was constructed
the member was destroyed; devices live went from 1 to 2 <- the body-acquired handle leaked
devices live went from 2 to 2 <- the guard_ member was destroyed, and released the device
Same class, same failure, one line moved from the body into a member. Part E is a destructor that has nothing in flight — so it may report, and must not throw — and Part F shows that a failed acquisition cannot leak at all, because the object never existed.
The final book-keeping is the lesson:
acquisitions 7
refused 1 (the acquisition threw before a resource existed)
releases 5
devices still live 2
Two devices are still live, on purpose, and each is one of the two mistakes above.
3. A hierarchy, and the three ways to make one useless¶
examples/module-13/03-custom-hierarchy-and-slicing/main.cpp
Predict first: a CorruptCatalogue exception is caught by catch (LibraryError error) — by
value. What exactly does the handler lose: the message, the type, the extra accessors, or all three?
Part A catches narrowly; Part B catches the base with a virtual classifier and keeps the detail; Part C answers the prediction:
Part C - catching by value loses the derived part
by value: kind LibraryError, category data
this handler cannot ask for file() or line(): the accessors
lived in the derived type, and the copy is a LibraryError.
Note what survived and what did not. The TEXT still names the
line, because the base built the message before the copy.
The CLASSIFICATION did not: kind() reports LibraryError ...
by const&: kind CorruptCatalogue, category data
file catalogue.txt, line 12 <- the context is intact
What to notice. This is why the mistake survives code review: nothing crashes, a message is printed, and the information the handler needed has quietly gone. The text survived because the base class built it before the copy — the type did not, because the type is not data.
Part D is the ordering mistake; Part E shows the difference between throw; (re-raises the same
object, type intact) and throw error; (copies it — and from a base reference, slices it again):
`throw;` has no operand and re-raises the current exception with its
type and data intact. `throw error;` would copy it - and, from a base
reference, slice it, which is Part C again.
4. The three guarantees, and noexcept¶
examples/module-13/04-guarantees-and-noexcept/main.cpp
Predict first: an operation validates its input after inserting. The validation throws. Is the container changed? Is anything leaked? Is the object still valid? (Three questions, and the surprising answer is yes, no, yes.)
Part B - BASIC: the failure happened, and so did the change
caught: copies cannot be negative: Bad Record
size before 2, after 3: Compilers, Algorithms, Bad Record
Part C - STRONG: work on the side, then commit
caught: copies cannot be negative: Bad Record
size before 2, after 2 <- unchanged
What to notice. The difference between the two functions is five lines, and it is not about
catch: it is about when the object is modified. The strong version does the work on a copy and
swaps, which is Module 10's idiom doing duty as a guarantee:
build a correct copy, then swap it in. `swap` is noexcept, so the last step
cannot fail - which is what turns 'do the work and hope' into
commit-or-rollback.
Part D checks four noexcept claims with static_assert, so the guarantee is written where the
compiler verifies it, and Part E explains the consequence of breaking the promise: termination
without unwinding, because the caller has already been told the call cannot fail.
5. When to throw, and when not to¶
examples/module-13/05-validation-and-design-rules/main.cpp
Predict first: four failures are coming — a number the user typed, a search that finds nothing, a request for a title the catalogue does not have, and a negative count. Predict which of the four mechanisms each deserves: validate, return, throw, or assert.
Part A - validate at the boundary, then stop worrying
'3' -> accepted, 3
'' -> refused: no value given
'seven' -> refused: not a number: 'seven'
Part B - return a value: the expected outcomes
find("Compilers") -> found, 2 copy/copies
find("Astrophysics") -> nothing matched
Part C - throw: what nothing local can repair
require() threw: no item titled 'Astrophysics'
What to notice. find() and require() are the same lookup with two different answers to the
question "who decides what happens when there is no match?" — the caller, or the operation. Naming
them differently is what makes the choice visible at the call site.
Part E quotes the standard library's own messages, and they are instructive precisely because they are poor:
shelf.at(4) threw std::out_of_range: vector::_M_range_check: __n (which is 4) >= this->size() (which is 2)
std::stoi("seven") threw std::invalid_argument: stoi
std::stoi("99999999999999999999") threw std::out_of_range: stoi
The standard fixes the type of a failure but leaves the message implementation-defined, and this
implementation's stoi message is the literally useless word stoi. Your what() strings are read
by people; write them for people.
6. The reproduction you must run yourself: std::terminate¶
This is the module's deliberate failure. It is not in examples/ because the program does not exit
normally, so it cannot be verified by the automated comparison. Save it as terminate.cpp and run it.
#include <iostream>
#include <stdexcept>
#include <string>
struct Cleaner {
std::string name;
explicit Cleaner(std::string n) : name(std::move(n)) {}
~Cleaner() { std::cout << " [cleanup] " << name << "\n"; } // buffered!
};
struct Loader {
~Loader() noexcept(false) {
throw std::runtime_error("cannot close the catalogue file");
}
};
int main() {
try {
Cleaner guard("catalogue file");
Loader loader;
throw std::runtime_error("the original failure");
} catch (const std::exception& error) {
std::cout << "caught: " << error.what() << "\n";
}
return 0;
}
$ g++ -std=c++17 -Wall -Wextra terminate.cpp -o terminate && ./terminate
terminate called after throwing an instance of 'std::runtime_error'
what(): cannot close the catalogue file
That is the entire output, and the compiler said nothing at all while building it — which is worth
noticing in itself, because a destructor declared noexcept(false) is a deliberate, visible statement
rather than an accident, and the compiler takes you at your word. Read what that transcript says,
because it is a debugging trap:
- The message names the wrong failure. The destructor's exception is reported;
the original failureis never mentioned, and thecatchblock never runs. - No cleanup line appears.
Cleaner's destructor did not get to print:std::terminatedoes not unwind, and[cleanup]was still sitting in a buffer that the abort discards. Addstd::endland it appears — which is the general rule: on an abnormal exit, anything still buffered is lost.std::cerris unbuffered and survives; that is why a diagnostic goes there. - The exit status is not 0. This shell reports 127 for the abort; on many systems you will see
134, which is
SIGABRT. Either way the process did not return frommain.
The same three symptoms appear if you mark a function noexcept and let it throw. This one the
compiler does catch, in advance:
#include <iostream>
#include <stdexcept>
void promisedNotToThrow() noexcept {
throw std::runtime_error("broke the promise");
}
int main() {
promisedNotToThrow();
std::cout << "never reached\n";
return 0;
}
$ g++ -std=c++17 -Wall -Wextra promise.cpp -o promise
promise.cpp: In function 'void promisedNotToThrow()':
promise.cpp:5:5: warning: 'throw' will always call 'terminate' [-Wterminate]
5 | throw std::runtime_error("broke the promise");
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$ ./promise
terminate called after throwing an instance of 'std::runtime_error'
what(): broke the promise
What to notice. Same warning text, two very different situations. Here the compiler saw the
throw inside a function that had promised not to throw and told you so at build time. In the
destructor case there was no warning, because the destructor said noexcept(false) out loud: the
promise was withdrawn, and the termination moved from a compile-time certainty to a runtime accident.
Under this course's -Werror build the warning is an error, which is the compiler refusing to let you
write the function that way at all — a far better outcome than a mystery abort three months later.
7. Where each example is used¶
| Program | Used by |
|---|---|
01-error-codes-versus-exceptions |
§1–2 of the concepts page, exercises W1 and C1 |
02-unwinding-and-cleanup |
§4 and §7 of the concepts page, debugging, Lab 13 requirement 5 |
03-custom-hierarchy-and-slicing |
§5–6 of the concepts page, misconceptions 3 and 6 |
04-guarantees-and-noexcept |
§8–9 of the concepts page, Lab 13 requirement 3, challenge X3 |
05-validation-and-design-rules |
§10 of the concepts page, exercises X1, challenge X1 |
| §6 (run it yourself) | debugging, self-assessment 6 |
Continue¶
Previous: Concepts and explanations · Next: Misconceptions and mistakes · Module home: Module 13