Skip to content

Module 4 — Lab 4: seal the system

Levels: [L2 Apply → L5 Design] · Extends: Lab 3 · Time: 4–5 hours


The scenario

The library system from Lab 3 works, and its object lifetimes are understood. Now the library has a review process, and the system has to pass it.

Two things have just been discovered in production:

  • a member was found holding minus one item. Nobody knows how it happened. The item count was on a screen, and someone "corrected" it.
  • a book was reported as both available and on loan at the same time, by two different screens. Both were reading the same object.

The review board asks one question: for each of your types, what exactly can a caller do to it? This lab answers that — by making the answer short, and by justifying every line of it in writing.

★ The written justification is half the lab. This is the first lab whose deliverable is as much a document as a program, and it is the shape of every design review you will ever take part in: here is my interface, here is why each member is public, here is what I deliberately did not provide.


What you start from

Your Lab 3 program: Book, Member and Catalogue in a header and a source file, with the limit of three enforced by Member, and the lifecycle instrumentation removed.

The command set is unchanged:

Command Meaning
A <id> <title> add an item
M <name> add a member
L <id> <member> lend item <id> to <member>
R <id> take it back
P print items and members
Q quit

Requirements

  1. State the invariant of every type, in one sentence each, as a comment immediately above the class — where a reader meets it first. A type with no invariant should say so explicitly rather than by omission.
  2. No public member that merely reads or writes a field. The test: for every public member, write the caller's sentence. "This needs to know … so that it can …" If the sentence ends nowhere, delete the member.
  3. Every operation that can fail refuses totally — it returns false and leaves every object involved exactly as it was. lend is the interesting one: it touches a Book, a Member and the catalogue.
  4. The deliberate failure-conflation from Lab 3 is resolved. If a caller can now tell which failure occurred — by a bool plus a query, or by an operation that can only fail one way — say how in the justification. (Throwing is Module 13's answer; a bool plus a query is fine here.)
  5. No leaks of the representation, checked against the four kinds in Concepts §5: no returned mutable reference, no iterator or pointer into storage, no signature naming a container or capacity as a storage detail, no const& return whose lifetime the caller can outlive.
  6. const-correctness. Every observing member function is const, and a free function taking const Catalogue& can print the whole system.
  7. A validation rule that did not exist before: a title may not be empty, and an identifier must be positive. Both are established at construction and checked by the operations that accept one.
  8. The attack test passes. Write the attacks from Debugging as a file, keep it, and comment it with what each attack is for.
  9. A written justification — justification.md — with one row per public member of every type.
  10. No warnings, and the Lab 3 command session still produces exactly the behaviour you expect (the counts will differ, because the reply text is now more precise — say so in the justification rather than leaving a silent change).

Requirement 10 is subtler than it looks

Sealing a system changes what its operations return, and therefore what it prints. The honest thing to do is to write down what changed and why, in one short section. A lab where outputs changed "somehow" is a lab that has not been finished.


What to deliver

  1. The sealed program, building cleanly.
  2. justification.md, with:
  3. each type's invariant, in one sentence;
  4. a table of public members: the promise it makes, and why it is public;
  5. a section listing the members you deleted and what a caller does instead — this is the part reviewers actually read;
  6. one member you are unsure about, with the argument on both sides;
  7. a short note on what changed in the program's output and why.
  8. attacks.cpp (or an equivalent section of your notes): the attacks you tried and the lines that now fail to compile, with a one-line comment each.
  9. The two production bugs, explained: for each, the line of your old code that allowed it, and the line of the new code that makes it impossible.

Self-assessment checklist

The invariant is real

  • [ ] Every type has its invariant stated above the class, in one sentence
  • [ ] Each invariant names the type that owns every rule in it — no rule is shared
  • [ ] Every operation that changes the object either preserves the invariant or refuses

The interface is honest

  • [ ] Every public member has a caller's sentence in justification.md
  • [ ] No public member returns a mutable reference, iterator or pointer into storage
  • [ ] No signature names a container, a size type or a capacity as a storage detail
  • [ ] The deleted-members section is longer than you expected

Refusals are total

  • [ ] A refused lend leaves the Book, the Member and the catalogue unchanged
  • [ ] A refused giveBack cannot make a member hold fewer items than it has
  • [ ] The fourth item for one member is refused while another member can still borrow
  • [ ] Each refusal's reason is available to the caller, or its uniqueness is explained

It is checked, not asserted

  • [ ] The attack file exists, its attacks are commented, and each either fails to compile or returns false
  • [ ] A const Catalogue& can be printed in full
  • [ ] Empty titles and non-positive identifiers are refused at construction and from the operations that accept one
  • [ ] The build has no warnings

It is explained

  • [ ] justification.md names the least certain member and argues both sides
  • [ ] Both production bugs are traced to a line in the old code and a line in the new
  • [ ] The output changes are documented rather than silently accepted

Common ways this lab goes wrong

Symptom Cause Fix
The justification table has a row per field The interface is still a public representation with function calls in front of it Rewrite the rows as operations; delete every row whose caller's sentence ends nowhere
Catalogue::find still returns Book* Lab 2 left it there and it "worked" That is the leak. Return a const Book*, or better, an operation: bool isOnLoan(int id) const, std::string describeItem(int id) const
lend refuses but the member's count was already incremented The operation checks, then mutates, then finds a second problem Check everything first, then mutate. Nothing may be written before all the checks have passed
Everything is const except one function, and it blocks printing That function both observes and modifies Split it, or accept a non-const reference in the printing function if it genuinely modifies — and say which in the justification
justification.md is a list of descriptions ("returns the title") Descriptions are not justifications Each row needs a promise and a reason: what a caller may rely on, and why they need it
Attacks "cannot be written" because the members are private The attack test is aimed at the interface Attack through the public members: borrow(""), borrow fifty times, giveBack something never held, lend an item that is already out
The program's output changed and nobody noticed Requirement 10 diff the new session output against Lab 3's and document every difference

Reference solution

examples/labs/lab-04/solution/ — main.cpp and justification.md.

Read them after your own version runs, and look for three specific things:

  • What the reference does not have. No setters, no getters that return storage, no way to set a count. The absences are the design, and the justification.md section on deleted members is the argument for them.
  • The Book type's describe() is flagged as arguable in the reference justification. A type that writes to std::cout has decided how it is presented, which is the caller's business — the reference keeps it for now and says why, and Module 16 replaces it. Your justification may reasonably disagree.
  • The comment showing the four attacks that used to compile (setOnLoan, setBorrowedCount with -4 and with kLimit + 10, and titlesHeld().push_back(...)). Those four lines are the before-and-after of this lab, and the reason the count is now derived rather than stored.

The reference solution's own Catalogue is not in this file — the lab's program is your Lab 3 program with these types. If your solution keeps a Catalogue::find returning a mutable pointer, that is the remaining leak, and finding it is Challenge X2.


Next: Challenge problems — interface design from caller sentences, and the immutability trade. · All modules