Skip to content

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:

void setBorrowedCount(int count) { borrowedCount_ = count; }   // the whole problem

Predict: after these two lines, what does alice.canBorrowMore() return, and what does bob.canBorrowMore() return?

alice.setBorrowedCount(-2);
bob.setBorrowedCount(99);
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 Member has no idea who else has what; that rule belongs to a Catalogue (Module 9).
  • describe() prints to std::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 - printSummary would 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:

  1. No operation names the storage. No items(), no begin(), no size type in a signature, no capacity returned as an arithmetical fact.
  2. 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:

const std::vector<std::string>& productNames() const;     // ← a representation leak

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

  1. Type out all four programs and predict their outputs, including which lines fail to compile.
  2. Run the Example 4 diff yourself, then add the leaking accessor and watch version 2 die.
  3. Do the Example 5 table for one of your own classes — a Vehicle, a BankAccount, your Lab 2 Book. Every row needs a caller's sentence, and any row whose sentence ends nowhere is a deletion.
  4. 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