Module 16 — Worked Examples¶
The application, built up in the order the module's concepts page uses: structure, then persistence, then robustness, then tests, then the report.
Every example prints a prediction prompt before its output. Answer it in a sentence before reading on — in this module more than any other, the prediction is where the learning is, because half of these failures produce no diagnostic that names their cause.
All programs are in examples/module-16/, and every diagnostic quoted was produced on this toolchain.
1. The file tree, and the ten-second question¶
Predict: a stranger opens this folder. Which file do they open first, and how long does it take them to be sure?
examples/module-16/01-multi-file-layout/
catalogue.h the public interface
catalogue.cpp its implementation
record_format.h internal declarations
record_format.cpp its implementation, with an unnamed-namespace helper
main.cpp the entry point
Catalogue (3 records, format # catalogue v1)
Compilers: Principles, Techniques, and Tools|Aho et al.|2006
The C++ Programming Language|Stroustrup|2013
Thinking in Systems \p A Primer|Meadows|2008
find: Stroustrup
the third record above contains a '|' and it is escaped, not lost
The insight. There is no utils.cpp and no manager.cpp. Every file's name is a responsibility, so
the read order is obvious from the listing — and a reader who opens main.cpp last has still understood
the program.
2. The include that is not there¶
Predict: main.cpp wants to print a record in the storage format. What happens if it calls
library::internal::format_record(...)?
// main.cpp
#include "catalogue.h" // and nothing else from the project
int main() {
// library::internal::format_record(book); // ← does not compile, and that is the design
}
The insight. The public header does not include the internal one, so the format is genuinely private: a caller cannot depend on it, which means changing it cannot break a caller. That is what "public interface" has to mean for the phrase to be worth anything.
3. Two translation units, one definition in a header¶
Predict — write down your answer: the header below defines a function without inline, and two
source files include it. What will the compiler or linker say, and which file and function will its
message name?
Nothing else. No file, no function, no "multiple definition" — the entire message is one line, and on its own it names nothing. The prediction prompt asked which function the message would name, and the answer is: none of them, on this toolchain.
The tool that answers the question is the symbol table:
The insight. T is "defined, external linkage", and there are two of them for one name: the
definition made its way into both translation units. When a link error explains nothing, read the
symbols — the whole of this module's link-error diagnosis is that one habit.
4. The same error, fixed twice¶
Predict: which of the two fixes below leaves the header's interface unchanged, and which changes it?
// Fix 1 — declare in the header, define in exactly one source file
int shelf_capacity(); // totals.h
int shelf_capacity() { return 40; } // totals.cpp — the only definition
// Fix 2 — mark the definition `inline`, so a definition per translation unit is legal
inline int shelf_capacity() { return 40; }
total_items : 81
is_empty : false
shelf_line : 3 boxes, 81 items, limit 40 (over the limit)
kShelfLimit : 40 (an inline variable)
an empty shelf: total 0, is_empty true
three translation units included totals.h and the link step found one definition of each name
The insight. Both are correct, and the choice is about the interface rather than about performance:
inline in modern C++ is an ODR permission, not a hint to the optimiser. Move the body to a source
file when the function is substantial or likely to change without its callers recompiling; keep it in the
header and mark it inline when it is genuinely part of the interface and small. In
examples/module-16/02-one-definition-fixed-twice/, total_items takes the first route, is_empty and
the C++17 inline variable kShelfLimit take the second, and both translation units include the header.
5. A helper in an unnamed namespace¶
Predict: this program has a header declaring library::internal::scale(int) and a source file
defining scale in an unnamed namespace. Will it link — and will the compiler say anything useful
before the linker does?
helper.cpp:8:5: error: 'int {anonymous}::scale(int)' defined but not used [-Werror=unused-function]
8 | int scale(int value) {
| ^~~~~
cc1plus.exe: all warnings being treated as errors
The insight. Read the symbol name: {anonymous}::scale(int). The header promised
library::internal::scale(int), and the definition created a name no other file can reach. The
compiler's unused function warning is the only diagnostic that discloses this, and it discloses it
completely — which is why compiling with -Wall -Wextra is not a style preference in a multi-file
project. The nm evidence, in the same program:
U library::internal::scale(int) # main.o — undefined, wants the global name
0000000000000000 t (anonymous namespace)::scale(int) # helper.o — lowercase t: LOCAL
A capital U looking for a lowercase t is this bug, every time.
6. Saving and loading, with a version marker and an escape¶
Predict: the third title contains a |, which is also the field separator. What comes back after a
save and load — the title, a truncated title, or an exception?
the file as written:
# catalogue v1
Compilers: Principles, Techniques, and Tools|Aho et al.|2006
The C++ Programming Language|Stroustrup|2013
Thinking in Systems \p A Primer|Meadows|2008
The insight. The format carries a version marker and escapes the separator inside a field, so
a record survives data that was not chosen by the programmer. A format without an escape corrupts the
first title containing |; a format without a marker cannot tell you that a file was written by an older
version of the program — and both failures arrive later, from a user, as "it lost my data".
7. An unchecked open that silently did nothing¶
Predict: the file does not exist. This program opens it, reads in a loop, and reports how many records it loaded. What does it print?
int loaded = 0;
std::ifstream in(path); // the file does not exist
std::string line;
while (std::getline(in, line)) { ++loaded; }
std::cout << "loaded " << loaded << " records\n";
A. unchecked open : loaded 0 records. No message, no crash, and the program believes the catalogue is empty.
B. checked open : refusing to continue - oop-course-stream-state.txt could not be opened
The insight. Line A is the worst failure in this module: not a crash but a plausible result. A
program that loads nothing and reports nothing looks like a working program with no data. One
if (!in.is_open()) converts it into line B, which a user can act on.
8. The same code against a file someone edited by hand¶
Predict: the loader in examples/module-16/05-untrusted-files/ is handed ten broken files. Which of
them does it refuse, and what does it say about each?
1. missing file : rejected - the file could not be opened
2. empty file : rejected - the file is empty
3. hand-edited header : rejected - line 1: expected '# catalogue v1', found '# catalogue v2'
4. count over the limit : rejected - line 2: declared 1000000 records, over the limit of 10000
5. count that is not a number: rejected - line 2: the declared count 'three' is not a number
6. count that does not fit : rejected - line 2: the declared count '99999999999999999999999' does not fit in a size_t
7. truncated record : rejected - line 3: malformed record 'Compilers|Aho'
8. corrupted field : rejected - line 3: malformed record 'Compilers|Aho|twenty thirteen'
9. a record deleted by hand: rejected - line 2: declared 3 records, found 1 - a record was added or deleted by hand
10. good file, blank line : accepted - loaded 3 records
The insight. Four of these are the same failure at different boundaries: a count from outside is untrusted (4), can be unparseable (5), can overflow the type it is going into (6), and can disagree with the data (9). Teaching input validation as one idea — "the file is hostile until each field has been checked" — is what turns ten special cases into three rules. And case 10 is deliberate: a blank line left by an editor is not corruption, and strictness in the wrong place is its own defect.
9. A round trip, compared field by field¶
Predict: the second round trip below saves a catalogue with no records. What does it print?
populated catalogue: 3 saved, 3 loaded, identical = true
the title containing '|' came back intact: true
empty catalogue : 0 saved, 0 loaded, identical = true
both round trips preserved every field: true
the empty case is the one that catches a loader that assumes a record
The insight. The empty catalogue is the case that catches the bugs — a loop that assumes a first record, a format that writes a marker and nothing else, a comparison that trivially passes because both sides are empty and the code never exercised the loader at all. A round trip without an empty case is evidence that the populated path does not delete things, which is not the same claim.
10. Three assertions, and one printing test¶
Predict: the implementation below has a cap one day too generous — 15 instead of 14. Will the printing test detect it?
// the printing "test"
std::cout << " fine for 10 overdue days (staff): " << schedule.pence_for(10, true) << "\n";
correct implementation:
fine for 10 overdue days (staff): 15
implementation with a cap one day too generous:
fine for 10 overdue days (staff): 15
Identical. The bug is invisible at that input, and the printing test could not detect it even at a visible input, because nothing compares the value to anything. Then the same suite, with assertions:
=== the same assertions, against the buggy implementation ===
ok 0 days costs nothing
ok 1 day costs 3p
ok the cap is reached at 14 days
FAILED 15 days is still capped at 42p
FAILED a year overdue is still 42p
buggy implementation: 6 passed, 2 failed
The insight. The two failing assertions are the boundaries — 15 days and 400 days — which is exactly where a cap can be wrong. A test suite's value is not in the number of lines it prints but in whether it fails when it should, and the way to know is to introduce the bug on purpose and watch.
11. Refactoring under test¶
Predict: a file is split in two, moving half its functions to a new source file. What has to be true of the test result for the refactoring to count as one?
The insight. Identical behaviour, proved by the suite passing unmodified. If a test had to be edited, either the behaviour changed or the test was asserting an implementation detail — and both are findings about the change, not chores to get past. This is also why the tests come before the refactoring: a green suite is the only licence you have to restructure working code.
12. A reversed decision, honestly reported¶
Predict: which part of a design report is hardest to write, and which part is the most convincing to a reader?
My decision: the catalogue stores items by value in a vector
The cost of that choice: lookups are linear; the code is simple and the items are contiguous
The option I rejected: an unordered_map keyed by id — faster lookups, and every iteration
site rewritten to walk the index
What would change my mind: several thousand items, or a lookup on a hot path
The decision I would reverse: modelling loans as a member of Item; reversing it means a separate
Loan collection and a migration of the persist format
The insight. The hardest line is the reversal, and the most convincing one is the cost of the reversal — because naming what it would take to change your mind proves you considered the change and declined it for a reason rather than preferring what you happened to build first. A report whose every decision is defended has not evaluated anything.
Continue¶
Previous: Concepts and explanations · Next: Misconceptions and mistakes · Module home: Module 16