Skip to content

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::borrow returns 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), and contents() const returning a std::string;
  • no public member that returns a container, a reference into storage, or a size type;
  • every operation that can fail returns a bool and 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:

  1. Which uses were genuinely wrong and are now caught?
  2. Which uses needed a new object rather than a modified one, and how did you express that?
  3. 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:

  1. Did the diff stay silent? If not, which difference did the caller observe — and which signature or behaviour leaked it?
  2. Which public member came closest to forcing a change in main, and why?
  3. 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:

  1. "show which slots are free on a given day"
  2. "book a slot for a member"
  3. "cancel a member's booking"
  4. "show everything a member has booked"
  5. "stop anyone booking the same court twice at the same time"
  6. "let staff extend a booking by 30 minutes"
  7. "show how much of each court's day is booked"
  8. "change a member's phone number"

Deliverable:

  1. One type per responsibility, with a one-sentence description containing no "and".
  2. 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.
  3. The invariants, one sentence each: which sentences must be true of every valid object, and which type owns each one.
  4. 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:

  1. CardNumber — a member's library card identifier
  2. Book — title, identifier, whether it is currently on loan
  3. LoanRecord — which member has which item, and the day it started
  4. Member — a name, a card number, and the items currently held
  5. Librarian — 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 Roster and 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 mutable cache 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 the mutable.
  • S3. Find a class in the standard library that offers both a const and a non-const overload of the same operation (a container's at or operator[] 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