Module 4 — Exercises¶
Every exercise states its level and its success criterion. Attempt before checking; reading a solution produces recognition, and recognition is not the skill being tested.
Several exercises here ask for a written justification rather than working code. That is deliberate: this module's skill is judgement, and a judgement you cannot write down is a preference.
Warm-up [L1 Understand]¶
W1 — three words, three jobs¶
Task. Give a one-sentence definition of each, and then classify the statements below:
- encapsulation
- abstraction
-
information hiding
-
"The caller cannot see how the items are stored."
- "
Catalogue::borrowreturns false if the item is already out." - "The class offers
isLongRead()instead of a page count." - "The private members could be replaced and no caller would need to change."
- "There is no way for a caller to set the borrowed count."
Success looks like. Three definitions that are not synonyms of each other, and five classifications with a one-line reason. Statements 1 and 4 are not the same thing — say how.
Stuck? Concepts §2 and §7.
W2 — capability or accessor?¶
Task. For each proposed public member, say which it is, and — if it is an accessor — what you would offer instead:
int pages() const;
void setOnLoan(bool);
bool isLongRead() const;
std::vector<Loan>& loans();
bool record(const Loan& loan);
void setBorrowedCount(int count);
int borrowedCount() const;
Success looks like. Seven verdicts, four of them with a named replacement, and one verdict that defends keeping an accessor (there is exactly one clear case in the list).
Stuck? Concepts §4 — the conversion table.
W3 — write the refusal¶
Task. A Shelf holds up to 20 books. Write the single operation that adds a book, with
the limit enforced, and state what it returns and what it does when it refuses.
Success looks like. One function, no setter, and — this is the marked part — a refusal that leaves the object completely unchanged, with a comment saying so.
Core [L2 Apply] · [L3 Modify] · [L4 Analyze]¶
C1 — an encapsulated collection [L2 Apply]¶
Task. Write Playlist:
- a name, fixed at construction;
- up to 12 tracks, added in order;
- operations:
addTrack,removeTrack,trackCount,canAddMore,contains(title), andcontents() constreturning astd::string; - no public member that returns a container, a reference into storage, or a size type;
- every operation that can fail returns a
booland changes nothing when it fails.
Then write a main that exercises the refusals as well as the successes.
Success looks like. Builds with no warnings; the refusal cases visibly change nothing; and a written sentence stating the invariant, using the word "at most".
Note. contents() returning a std::string rather than a std::vector<std::string> is
the exercise's actual lesson. If you find yourself needing the vector, write down the
caller's sentence for it — and check whether the sentence is really "display the tracks".
C2 — attack this class [L4 Analyze]¶
Task. Here is a class somebody submitted. Run the attack test on it.
class Roster {
public:
std::string& at(int index) { return names_[index]; }
int count() const { return count_; }
void setCount(int count) { count_ = count; }
bool add(const std::string& name) {
names_[count_++] = name;
return true;
}
bool kick(int index) {
for (int i = index; i + 1 < count_; ++i) {
names_[i] = names_[i + 1];
}
--count_;
return true;
}
private:
std::string names_[10];
int count_ = 0;
};
Find every way a caller can put this object into a state its own code does not expect. For each attack: the line, the state it produces, and the minimal fix.
Success looks like. At least five distinct attacks, including one that does not involve
setCount and one that produces undefined behaviour rather than a wrong value.
Full credit includes noticing that add and kick both need a guard the class does
not have, and that no amount of caller-side care substitutes for them.
C3 — make a value immutable, and see what breaks [L3 Modify]¶
Task. Take this mutable type and make it immutable:
class Temperature {
public:
Temperature(double celsius) : celsius_(celsius) {}
double celsius() const { return celsius_; }
void setCelsius(double value) { celsius_ = value; }
private:
double celsius_;
};
Make the member const, delete the setter, and build every program you have that uses it —
recording each line that stops compiling. Then answer:
- Which uses were genuinely wrong and are now caught?
- Which uses needed a new object rather than a modified one, and how did you express that?
- Was immutability right for this type? Argue both sides in three sentences and decide.
Success looks like. A compile log of what broke, the two categories, and a defended decision — not a reflex in either direction.
Full credit for (3) distinguishes a temperature reading from a thermostat setpoint. The two want different designs, and saying which this class is was the missing requirement.
C4 — swap the representation [L3 Modify]¶
Task. Take your C1 Playlist and add a second private representation behind a
compile-time flag — for example a linked structure, or parallel arrays, or a counter plus a
fixed array. Build both versions and diff their output on the same main.
g++ -std=c++17 -Wall -Wextra main.cpp -o playlist_v1
g++ -std=c++17 -Wall -Wextra -DPLAYLIST_VERSION=2 main.cpp -o playlist_v2
./playlist_v1 > out1.txt && ./playlist_v2 > out2.txt && diff out1.txt out2.txt
Then answer, in writing:
- Did the diff stay silent? If not, which difference did the caller observe — and which signature or behaviour leaked it?
- Which public member came closest to forcing a change in
main, and why? - What would you have had to add to the interface to make the second representation impossible?
Success looks like. A silent diff (or an honest account of the leak), and answers that name specific members.
Stuck? Concepts §7 and
examples/module-04/04-representation-swap/main.cpp.
C5 — a const-correctness audit [L4 Analyze]¶
Task. Take a class of your own with at least five member functions. For each function,
state whether it should be const, and why — then make the change and record what the
compiler says.
Then find the payoff: write a free function that takes const YourClass& and uses as many
of those functions as possible. Any function that blocks it is either wrongly non-const,
or is an operation the free function should not be doing.
Success looks like. A table of functions with a const-or-not decision and a reason each; the free function; and a note on the one function that made you think.
Challenge [L4 Analyze] · [L5 Design]¶
X1 — design an interface from caller sentences [L5 Design]¶
Task. A campus sports centre needs a booking system. Here is what the user interface needs, said in the words of the person requesting it:
- "show which slots are free on a given day"
- "book a slot for a member"
- "cancel a member's booking"
- "show everything a member has booked"
- "stop anyone booking the same court twice at the same time"
- "let staff extend a booking by 30 minutes"
- "show how much of each court's day is booked"
- "change a member's phone number"
Deliverable:
- One type per responsibility, with a one-sentence description containing no "and".
- For each of the eight requirements, the public member that satisfies it — or a written refusal, if the requirement should not become an interface member as stated.
- The invariants, one sentence each: which sentences must be true of every valid object, and which type owns each one.
- Two requirements you would go back and ask about, with the question you would ask.
Success looks like. Every requirement traced to an operation or explicitly refused; invariants that name exactly one owner each; and questions that would change the design.
How you will be judged. On (2) and (4). Requirement 8 is the trap: "change a phone number" is a setter until you ask what else a change of phone number involves (validation, notification, duplicates) — and requirement 5 cannot be enforced by the type that only knows about one booking.
X2 — the leak inventory [L4 Analyze]¶
Task. Six public members from six different submitted classes. For each: name the kind of leak, say what a caller can do that they should not, and give the minimal fix.
std::vector<Book>& books(); // (a)
const std::vector<Book>& books() const; // (b)
std::vector<Book>::iterator begin(); // (c)
Book* find(int id); // (d)
bool setTitle(const std::string& title); // (e)
std::size_t capacity() const; // (f)
Success looking like a strong answer: six named leak kinds, six consequences, six fixes — and at least one where you argue that the member is acceptable under a stated condition.
Full credit includes distinguishing (a) from (b) from (c): only one of the three is
refused by the compiler when the function is marked const, and that is not the one that
leaks the most.
X3 — the immutability decision, five times [L5 Design]¶
Task. For each type below, decide immutable (all members const, no setters) or
mutable with enforced operations, and defend the choice in two sentences:
CardNumber— a member's library card identifierBook— title, identifier, whether it is currently on loanLoanRecord— which member has which item, and the day it startedMember— a name, a card number, and the items currently heldLibrarian— a name and the desk they are currently at
For each, name the cost of your choice: for immutable, what operations the type can no longer support; for mutable, which rule now needs enforcing and where.
Success looks like. Five decisions, five costs named, and at least one where further thought changed your mind from your first instinct.
How you will be judged. On the costs. Immutability is not "safer"; it is a trade — it buys freedom from change and pay with loss of assignability, and the types above are chosen so that the trade differs in each case.
Stretch (optional)¶
- S1. Take C2's
Rosterand rewrite it so that no attack you found is possible, using the fewest public members you can. Count them. A correct rewrite usually has three. - S2. Add a
mutablecache to one of your classes that computes something expensive, and write three sentences justifying it against the test in M5. If you cannot satisfy the test, remove themutable. - S3. Find a class in the standard library that offers both a
constand a non-constoverload of the same operation (a container'satoroperator[]will do). Explain, in three sentences, why two functions are needed and what the compiler uses to choose between them.
Solutions¶
| Exercise | Reference |
|---|---|
| W2, W3 | examples/module-04/02-enforcing-invariants/main.cpp — the conversions and the refusal, implemented |
| C1 | The same program: the Member type is this exercise's structure with a different noun |
| C2 | Misconceptions, D4 and D5 name two of the attacks; the rest are yours to find |
| C3 | examples/module-04/03-const-and-immutability/main.cpp — CatalogueNumber is the immutable version, and its comment shows what refusing at construction looks like |
| C4 | examples/module-04/04-representation-swap/main.cpp — two representations, one silent diff |
| C5 | The const and non-const split of Basket in the same program |
| X1, X3 | examples/labs/lab-04/solution/justification.md — the reference table, with its uncertainties left visible |
C2 has no published solution on purpose: it is an analysis exercise and the list you produce is the answer. Compare it with the reference table afterwards — the interesting column is the attacks you missed.
X2 has one deliberately arguable row. Choosing which it is, and defending the condition under which that member is acceptable, is the exercise.
Next: Lab 4 — seal the running system, and justify every public member. · All modules