Module 13 — Exercises¶
Every exercise states its cognitive level, its task, and the observable result that counts as success. Work in a scratch directory and compile with the course's flags, treating a warning as a failure:
Two habits, from this module on: print diagnostics to std::cerr, so that an abort cannot swallow
them, and make failures happen on purpose rather than waiting for them.
Warm-up [L1 Understand]¶
W1 — One failure, thrown and caught¶
Task. Write a function double averageOf(const std::vector<double>& values) that throws
std::invalid_argument("averageOf: no values") for an empty vector. Call it from main twice: once
with data, once empty, catching the failure and printing what().
Success looks like. Two lines — the average, then averageOf: no values — and the program exits
with status 0, which proves the exception did not escape main.
Stuck? Section 3 of Concepts.
W2 — Predict the unwinding trace¶
Task. Before compiling, write down the exact output of this program. Then run it and compare.
#include <iostream>
#include <stdexcept>
#include <string>
struct Guard {
std::string name;
explicit Guard(std::string n) : name(std::move(n)) { std::cerr << "+ " << name << '\n'; }
~Guard() { std::cerr << "- " << name << '\n'; }
};
void work() {
Guard a("a");
Guard b("b");
Guard c("c");
throw std::runtime_error("failed after three guards");
}
int main() {
try {
work();
} catch (const std::exception& error) {
std::cerr << "caught: " << error.what() << '\n';
}
}
Success looks like. The predicted trace is + a, + b, + c, - c, - b, - a, then
caught: failed after three guards — and your prediction matched. If it did not, say in one sentence
why the order is reversed, not merely that it is.
Stuck? Section 4 of Concepts.
W3 — A three-level hierarchy with useful messages¶
Task. For the library system, write LibraryError (derived from std::runtime_error) with a
kind() accessor, and two derived types: ItemUnavailable (carrying the item title) and
MemberNotFound (carrying the member id). Throw each from a short function, and catch them twice:
once by the specific types, once by the base.
Success looks like. Two runs of the same code with different handlers: the specific one prints the
class and the context; the base one prints kind() and the message. Every what() names the item or
the id — a message like "invalid input" fails this exercise.
Stuck? Section 6 of Concepts and
examples/module-13/03-custom-hierarchy-and-slicing/.
W4 — Classify six operations¶
Task. For each of these, state the guarantee — nothrow, strong or basic — and give the reason:
Catalogue::size() constCatalogue::add(const Item&)implemented with copy-and-swapCatalogue::addAndNotify(const Item&)which pushes back, then calls a virtualnotify()Catalogue::operator[](std::size_t)(no bounds check)Catalogue::swap(Catalogue&)Catalogue::~Catalogue()
Success looks like. Six classifications, each one sentence. Two of them should be nothrow, and the reason for one of those is a language rule rather than a design choice — name it.
Stuck? Sections 8 and 9 of Concepts.
W5 — Two handlers, one of them dead¶
Task. Write a handler list where a std::runtime_error handler is written before an
ItemUnavailable handler. Compile it, read the warning, then reorder the handlers and confirm the
derived handler runs.
Success looks like. The predicted warning —
warning: exception of type 'ItemUnavailable' will be caught by earlier handler [-Wexceptions]
note: for type 'std::runtime_error'
— followed by the corrected run, and one sentence stating the ordering rule.
Stuck? Section 5 of Concepts, mistake M2 in Misconceptions and mistakes.
Core [L3 Modify → L4 Analyze]¶
C1 — "Fix this": caught by value, in the wrong order¶
Task. The program below reports two different failures as one. Find both defects, fix them, and
prove the fix: after your change, the two failures must print different kind() values.
class LibraryError : public std::runtime_error { /* ... kind(), category() ... */ };
class CorruptCatalogue : public LibraryError { /* ... file(), line() ... */ };
int main() {
try {
throw CorruptCatalogue("catalogue.txt", 12, "unexpected token");
} catch (const std::exception& error) { // defect 1: the general handler is first
std::cerr << "general: " << error.what() << '\n';
} catch (LibraryError error) { // defect 2: by value, AND now unreachable
std::cerr << error.what() << '\n';
}
}
Success looks like. Two one-line explanations that name the mechanisms (handler order and
slicing), the corrected code, and output in which the CorruptCatalogue handler runs and kind()
reports CorruptCatalogue rather than LibraryError. Note that fixing only the ordering leaves the
slicing defect: with the handlers swapped, catch (LibraryError error) still loses the derived part.
Stuck? Sections 5 and 6 of Concepts.
C2 — Fix the leak without adding a single catch¶
Task. This function leaks when load() throws. Fix it — and the constraint is the exercise: no
catch block, no try, and no delete.
struct Device { int handle; };
void reload(const std::string& path) {
Device* device = acquire(path); // may throw during load()
load(device);
release(device);
}
Success looks like. The rewritten function is shorter than the original, its counters balance on the success and failure paths, and one sentence says what property of C++ makes it work.
Stuck? Section 7 of Concepts and Module 11.
C3 — Give an operation the strong guarantee, and prove it¶
Task. Catalogue::importAll appends every item from a list, validating each one and throwing on
the first invalid entry. As written it is only basic-guarantee. Make it strong, then prove it:
Catalogue catalogue;
catalogue.add("Compilers");
const std::size_t before = catalogue.size();
std::vector<Item> incoming{{"Algorithms", 2}, {"Bad Record", -1}};
try {
catalogue.importAll(incoming);
} catch (const std::invalid_argument& error) {
std::cerr << "rejected: " << error.what() << '\n';
}
assert(catalogue.size() == before); // must hold after your fix
Success looks like. The assertion passes, the catalogue's contents are unchanged, and the comment names the technique (work on a copy, then swap) and why the last step cannot fail.
Stuck? Section 8 of Concepts and
examples/module-13/04-guarantees-and-noexcept/.
C4 — A terminate message that names the wrong failure¶
Task. Given this program, explain the observed output before running it, then make it behave correctly — with the error still reported.
struct CatalogueFile {
~CatalogueFile() noexcept(false) { throw std::runtime_error("cannot close the file"); }
};
int main() {
try {
CatalogueFile file;
throw std::runtime_error("malformed record at line 12");
} catch (const std::exception& error) {
std::cerr << "caught: " << error.what() << '\n';
}
}
Success looks like. The correct prediction (terminate called ... what(): cannot close the file,
with no caught: line), a one-sentence explanation of why the first failure is unrecoverable, and a
repair in which the destructor reports through std::cerr or an explicit close() instead of
throwing — and the catch block finally runs.
Stuck? Debugging, Moves 1 and 2.
C5 — Mark exactly what cannot throw¶
Task. Add noexcept to the functions that deserve it in your own Catalogue, then check the
claim rather than asserting it in a comment:
static_assert(noexcept(std::declval<const Catalogue&>().size()),
"size() is advertised as nothrow");
static_assert(!noexcept(std::declval<Catalogue&>().add(std::declval<const Item&>())),
"add() can throw, and the header should say so");
Success looks like. At least three static_asserts, at least one of them negative (a function
that must not be noexcept), and one sentence explaining what the standard library does
differently because of a noexcept move.
Stuck? Section 9 of Concepts.
Challenge [L4 Analyze → L7 Refactor]¶
X1 — A policy for six operations¶
Task. For these six operations of the library system, decide throw / return a value / validate and refuse / assert, and justify each in one sentence:
- borrowing an item that is already on loan
- searching the catalogue for a title that is not held
- loading a catalogue file that is malformed
- computing a fine for a loan whose due date is missing
catalogue.at(index)whereindexcame from a form the student typedcatalogue.at(index)inside a loop whose bounds you computed ten lines earlier
Success looks like. Six decisions, each defended by who can fix it and how often it happens — and at least one decision where returning a value is right and one where throwing is, for the same kind of lookup. If all six answers are "throw", the exercise has not been done.
Stuck? Section 10 of Concepts.
X2 — Add a failure path, and a test that exercises it¶
Task. Take your Lab 13 program and add a failure path for a corrupted input file: the program must detect it, report what was wrong including the line number, leave its state unchanged, and continue running rather than aborting. Then write the test that proves it, by making the corruption happen on purpose.
Success looks like. Three artefacts: the detection code, the transcript of the failing run (with the counters balancing), and a passing assertion that the state is unchanged. A version that crashes on the bad file, then the fixed version, and one sentence per line of the malformed file that explains what the reader is being told.
Why this is the exit-level exercise. It is the whole module in one deliverable: hierarchy, validation at the boundary, the strong guarantee, RAII, counts on both paths, and a failure path that has actually run.
Stuck? Lab 13 and Debugging, Move 4.
X3 — Refactor error codes to exceptions — then argue against yourself¶
Task. Take a program of yours that reports failures with return codes (or write a 60-line one). Refactor it to exceptions. Then find one place where the return code was genuinely better and say why, using the categories from section 10 of the concepts page.
Success looks like. A refactor in which the intermediate frames got simpler, and an honest exception to the rule with a concrete reason (an expected, frequent, non-exceptional outcome; a callback whose failure is the caller's business; a hot loop). "Exceptions are modern" is not a reason; "this failure is the normal answer to this query, and it happens thousands of times per run" is.
Stuck? Sections 1, 2 and 10 of Concepts.
How to check yourself¶
- [ ] Every exercise compiles with
-Wall -Wextra -Werror, with no warnings - [ ] Every prediction was written down before the corresponding run
- [ ] Every failure path was produced deliberately, not awaited
- [ ] Every claim of a guarantee is backed by a checked state, not a comment
- [ ] The counters balance on both the success and the failure run
- [ ] Diagnostics go to
std::cerr