Module 4 — Worked Examples¶
Four programs and one design exercise. Each program is real, builds with the course's
command, and its captured output sits beside it in examples/module-04/.
The rule for this page: predict the output — and, more importantly, predict which lines compile. Two of the four programs contain code that does not compile, deliberately, and the exact error is part of the lesson.
Example 1 — the class that is private and not encapsulated¶
examples/module-04/01-broken-encapsulation/main.cpp
Every member is private. The class states its own limit. And it is broken from outside in one line per rule:
Predict: after these two lines, what does alice.canBorrowMore() return, and what does
bob.canBorrowMore() return?
Output (run it first)
Both start in a valid state:
Alice: 0 item(s) out of 3, can borrow more? yes
Bob: 0 item(s) out of 3, can borrow more? yes
Breaking the rule from outside, in one line each:
Alice: -2 item(s) out of 3, can borrow more? yes
Bob: 99 item(s) out of 3, can borrow more? no
The object is now nonsense, and every function that trusts it
will produce nonsense too:
alice says 'can borrow more'? yes (with -2 items out, the answer is meaningless)
bob says 'can borrow more'? no (correct for this number, but the number is a lie)
Read alice's answer again: can borrow more? yes, with minus two items out. The
object is not merely wrong, it is wrong in a way that produces a confident answer. Every
function downstream of borrowedCount_ now inherits the lie, and none of them can tell.
bob is more instructive still: his answer (no) is correct for the number he holds.
The number is the lie. That is why "validate at the call site" does not work — the call
sites that matter are the ones that trust the value, and they are not where the damage was
done.
Ask yourself the question this program exists to provoke: if a setter can break the rule, what exactly was private for? Answer: the storage was hidden, and the rule was not protected — and the rule is the only thing that mattered.
Example 2 — the same class, with the rule enforced¶
examples/module-04/02-enforcing-invariants/main.cpp
Identical data. Five operations instead of four, and no way to set a count:
bool borrow(const std::string& itemTitle); // refuse, and change nothing
bool returnItem(const std::string& itemTitle); // the mirror, also checked
int itemCount() const;
bool canBorrowMore() const;
std::vector<std::string> borrowedTitles() const; // a COPY, deliberately
Predict two things: what borrow returns for a fourth item, and what the caller
receives from borrowedTitles() — a copy, or a reference to the object's own storage?
Output (run it first)
Borrowing, as the system means it:
borrow "C++ Primer": ok
borrow "Clean Code": ok
borrow "UML Distilled": ok
The limit is enforced by the class, not by the caller:
borrow a fourth: failed
Alice: 3/3 - C++ Primer Clean Code UML Distilled
Returning is symmetric, and also checked:
return "Clean Code": ok
return "Clean Code" again: failed
Alice: 2/3 - C++ Primer UML Distilled
can borrow more now? yes
A copy of the titles, so nothing inside can be reached:
2 title(s) received by the caller
the object is unchanged: Alice: 2/3 - C++ Primer UML Distilled
Bob: 0/3 (nothing out)
Bob: 1/3 - Clean Code
The fourth borrow fails, and nothing changed. Compare Alice: 3/3 before and after:
the refusal is total. This is what "a refused operation changes nothing" means in practice,
and it is what lets a caller attempt something without having to repair the object
afterwards.
The double return also fails, and that is the more interesting one. Two different failures — "you already hold three" and "you are not holding that" — both reported as a refusal. At this stage of the course that is acceptable; Modules 8 and 13 make the distinction available to the caller. What matters now is that neither failure leaves the object changed.
borrowedTitles() returns a copy. The comment in the source says why, and it is worth
copying into your own head:
// Returning `const std::vector<std::string>&` would let a caller keep a reference that
// outlives this object; returning a non-const reference would let them push items in
// behind the invariant's back.
std::vector<std::string> borrowedTitles() const { return borrowed_; }
Now the honest part. This class is not finished, and you should be able to say what is still missing:
borrow("")would be accepted — an empty title is not a book. There is no rule about titles yet. Lab 4 requires one.- Two members can hold the same title. The
Memberhas no idea who else has what; that rule belongs to aCatalogue(Module 9). describe()prints tostd::cout, so the type decides how it is presented — a leak of a different kind, and one the module's reference justification admits to.
None of these is a flaw in the mechanism. They are the next layer of rules, and each will be enforced the same way: one place, checked by the operation that can break it.
Example 3 — an immutable value, and a const-correct interface¶
examples/module-04/03-const-and-immutability/main.cpp
Two types with opposite designs, chosen deliberately:
class CatalogueNumber { // a VALUE: settled at birth, never changed
public:
CatalogueNumber(std::string prefix, int serial); // throws if serial <= 0
const std::string& prefix() const;
int serial() const;
std::string formatted() const;
private:
const std::string prefix_;
const int serial_; // const — no setter can exist
};
class Basket { // something with a life: it changes
public:
void add(const std::string& itemTitle);
int itemCount() const;
std::string mostRecentlyAdded() const;
private:
std::string owner_;
int itemCount_ = 0;
};
Predict: what does this program print for number.formatted(), and what happens if you
change CatalogueNumber number("LIB", 0);?
Output (run it first)
1. An immutable value type, whose invariant is set at birth:
LIB-1001
2. A const object can still be read:
Nadia has 0 item(s); most recently added: nothing yet
3. Changeable state, with the interface split in two:
Imran has 0 item(s); most recently added: nothing yet
Imran has 2 item(s); most recently added: Clean Code
A single const reference works for every kind of object:
Nadia has 0 item(s); most recently added: nothing yet
Imran has 2 item(s); most recently added: Clean Code
temporary has 0 item(s); most recently added: nothing yet
What the caller was never told: how any of it is stored.
itemCount_ could become a vector, a counter, or a query -
printSummary would not change by one character.
const members are a design statement with teeth. CatalogueNumber has no setter
because it cannot have one — the members are const, so assignment is impossible. The
invariant "the serial is positive" is established at construction (and refused, loudly, for
zero: the program's comment shows the std::invalid_argument you get). A value type like
this is safe to copy, compare, store and pass around precisely because nothing can happen to
it after birth.
One const& parameter serves every kind of object. printSummary(const Basket&) is
called with a const Basket, with a mutable Basket, and with a temporary. That is the
payoff of splitting the interface into const and non-const members — and it would be
impossible if itemCount() were not const.
Look at what the last paragraph of the output claims:
itemCount_could become a vector, a counter, or a query -printSummarywould not change by one character.
That is the representation-swap test stated as a prediction. Example 4 performs it.
Example 4 — the same interface, two representations, one output¶
examples/module-04/04-representation-swap/main.cpp
One public interface for a product inventory (add, setQuantity, quantityOf,
distinctProducts, totalUnits, report) and two completely different private
representations, chosen at compile time:
#if INVENTORY_VERSION == 1
std::vector<std::pair<std::string, int>> items_;
#else
std::string names_[kMaxProducts];
int quantities_[kMaxProducts] = {0};
int count_ = 0;
#endif
Build both and diff them:
g++ -std=c++17 -Wall -Wextra main.cpp -o inventory_v1
g++ -std=c++17 -Wall -Wextra -DINVENTORY_VERSION=2 main.cpp -o inventory_v2
./inventory_v1 > out1.txt && ./inventory_v2 > out2.txt && diff out1.txt out2.txt
Predict: does the diff produce anything at all? Then run it.
Output — identical for both builds (run it first)
Filling the inventory:
add "C++ Primer" x4: ok
add "Clean Code" x2: ok
add "UML Distilled" x3: ok
add more "C++ Primer" x1: ok
3 product(s), 10 unit(s)
C++ Primer x5
Clean Code x2
UML Distilled x3
Refusals - each changes nothing at all:
add "C++ Primer" x0: refused
add "" x5: refused
setQuantity "Unknown" to 7: refused
setQuantity "Clean Code" to -1: refused
3 product(s), 10 unit(s)
C++ Primer x5
Clean Code x2
UML Distilled x3
Queries:
C++ Primer in stock: 5
Clean Code in stock: 2
a product we do not carry: 0 (not an error)
distinct products: 3
total units: 10
Correcting and removing:
setQuantity "Clean Code" to 5: ok
setQuantity "UML Distilled" to 0 (remove): ok
2 product(s), 10 unit(s)
C++ Primer x5
Clean Code x5
The diff is silent, and that silent diff is the module's proof. Not one line of the caller changed. Two properties had to hold for that:
- No operation names the storage. No
items(), nobegin(), no size type in a signature, no capacity returned as an arithmetical fact. - No operation's behaviour depends on the representation. The capacity limit is
declared as part of the contract (
kMaxProducts) and enforced by both versions — so it cannot be observed as an artefact of the arrays.
Property 2 is the one people miss. A leak is not only a signature that names
std::vector; it is also any observable difference in behaviour. If version 2 refused the
ninth product and version 1 did not, the caller could tell them apart — and the array
representation would have leaked through the contract.
Now break it on purpose. Add this member to the public section:
Version 2 stops compiling, and it cannot be repaired without inventing a vector to return — which is exactly the cost the module is describing. Lab 4 asks you to find the same kind of leak in your own code and in the reference solution.
Example 5 — designing an interface, member by member¶
No program this time. A design, worked through the way Lab 4 requires, because the deliverable in this module is often a written justification rather than code.
Take the Member type from Example 2 and ask, for each candidate public member: what would
a caller do with this?
| Candidate | The caller's sentence | Verdict |
|---|---|---|
int borrowedCount() const |
"so I can check whether they may borrow another" | Replace with canBorrowMore() const — the comparison belongs here |
bool canBorrowMore() const |
"to decide whether to offer the borrow button" | Keep — it answers the question that exists |
void setBorrowedCount(int) |
"so I can correct the count" | Delete. Correct it from what? No correct caller needs this, and it makes the invariant unenforceable |
bool borrow(const std::string&) |
"the operation the system performs" | Keep — and it refuses rather than half-applying |
std::vector<std::string>& titles() |
"so I can list them" | Delete. Returns mutable references into the object's storage |
std::vector<std::string> borrowedTitles() const |
"so I can display them" | Keep, adjusted — a copy; and if display is all the caller wants, std::string titlesText() const is narrower still |
void describe() const |
"to print the member" | Reconsider. The type has decided how it is presented; a string return would be testable. Module 16 makes this change |
The full reference version of this table — one row per public member, with the promise it
makes and why it is public — is
examples/labs/lab-04/solution/justification.md. Read it after writing your own; the
interesting part is not the verdicts but the reasons, and where yours differ.
Two things to notice about the exercise itself:
- Deletions outweigh subtractions. The interface got smaller, not larger. A design review that only adds is not a review.
- One row is arguable (
describe()), and the reference justification says so, explains the cost, and keeps it for now. A design document in which nothing is uncertain is usually a design document that has not been thought about.
What to do with this page¶
- Type out all four programs and predict their outputs, including which lines fail to compile.
- Run the Example 4 diff yourself, then add the leaking accessor and watch version 2 die.
- Do the Example 5 table for one of your own classes — a
Vehicle, aBankAccount, your Lab 2Book. Every row needs a caller's sentence, and any row whose sentence ends nowhere is a deletion. - Then, before the lab, try to break your own class from outside. Whatever you succeed at is the lab. That is the whole of the next page's technique.
Next: Misconceptions and mistakes — eight wrong mental models, and the errors and silences this module produces. · All modules