Skip to content

Module 3 — Exercises

Every exercise states its level and its success criterion. Write your attempt before reading anything else; recognition is not the skill being tested.

The exercises build on the module's four programs in examples/module-03/. Have them open — several of them ask you to compare your answer with a captured trace rather than with an opinion.


Warm-up [L1 Understand]

W1 — order the lines

Task. Given the class and program below, list the print statements in the exact order they appear. Then run it.

#include <iostream>

class Meter {
public:
    Meter(const std::string& label) : label_(label) {
        std::cout << "construct " << label_ << '\n';
    }
    ~Meter() { std::cout << "destroy " << label_ << '\n'; }
private:
    std::string label_;
};

int main() {
    Meter outer("outer");
    {
        Meter inner("inner");
    }
    Meter last("last");
    std::cout << "end of main\n";
}

Success looks like. Five lines in the right order, plus one sentence on where "end of main" falls and why. If you predicted the destruction of inner at the end of main, re-read §7 of Concepts.

W2 — two members that must be initialised in the list

Task. For each member below, say whether it can be assigned in the constructor body, and if not, the reason in one sentence:

  1. int count_;
  2. const int id_;
  3. std::string& name_;
  4. Slot slot_; where Slot has only explicit Slot(int)
  5. static int total_;

Success looks like. Five answers. Numbers 2 and 3 have the same underlying reason stated twice — that is the point of asking both. Number 5 is a different kind of thing entirely, and saying which kind is half the mark.

Stuck? Concepts §4, and D1–D3 in Misconceptions.

W3 — which of these is a default constructor?

Task. For each, say whether it is a default constructor, and whether it suppresses the compiler's generated one:

Loan();
Loan(int days = 14);
Loan(int days);
Loan() = default;
Loan(const std::string& member = "(unassigned)");

Success looks like. Five verdicts with a one-line reason each. Three of the five are default constructors, and the fourth verdict (= default) needs a second sentence: suppressing nothing, but possibly still deleted.

Stuck? M4 in Misconceptions.


Core [L2 Apply] · [L3 Modify] · [L4 Analyze]

C1 — a class that cannot be built carelessly [L2 Apply]

Task. Write Locker for a campus locker system:

  • a locker number, fixed for the object's whole life and never changeable;
  • the member who rents it, as a reference that cannot be reseated;
  • a bool occupied() and a void describe() const;
  • a delegating constructor so that Locker(unassigned, someone) exists;
  • no constructor that can produce a locker with number 0.

Use it in a main that creates three lockers (one through the delegating constructor) and prints them.

Success looks like. Builds with no warnings under g++ -std=c++17 -Wall -Wextra; a const number and a reference member, both initialised in the list; and a written sentence saying which two of your members made the constructor body useless for initialisation.

Note. This exercise deliberately forces the two hardest cases of §4 into one class. If you find yourself wanting to assign either member in the body, that is exactly the error the compiler will refuse.

C2 — predict a trace, then explain every difference [L3 Modify]

Task. Take the class you wrote for C1 (or your Module 2 Vehicle/BankAccount) and:

  1. add the four-line instrumentation from Debugging — construction, copy, destruction, and a static live counter;
  2. write down the exact expected trace for a main with: two objects in the outer scope, one passed by value to a function, one created inside a nested { } block, and one returned from a function that returns by value;
  3. run it and produce a table: predicted line, actual line, and the mechanism for every difference.

Success looks like. A prediction of at least twelve lines written before the run; a trace; and a difference table where every row names a mechanism from the list in Worked examples — by-value parameter, temporary, elision, reverse order, delegation.

This is Lab 3's core skill. Doing it here, on a class you already know, makes the lab about the system rather than about the technique.

C3 — the const member, and its two errors [L3 Modify]

Task. Write a class with a const std::string code_; member, and initialise it in the constructor body instead of the list. Build it and record both errors verbatim. Then fix it, and confirm the fix is not "remove const".

Success looks like. Both messages, quoted exactly, matched against D1 in Misconceptions — and one sentence on why making the member non-const would be the wrong fix.

C4 — reproduce the reorder warning, deliberately [L3 Modify] (ticklish)

Task. Write Rectangle with width_, height_ and area_, initialising all three in the list but writing the list in a different order from the declarations. Build with -Wall -Wextra (no -Werror) and capture the three warning lines. Then:

  1. fix the list order, and confirm the warnings disappear;
  2. move int area_; above int width_; and, with the list in declaration order, show that area_ is now computed from uninitialised members. Explain the value you actually got.

Success looks like. The three-line -Wreorder warning captured verbatim; a clean build after the fix; and a demonstration that "correct list order" and "correct declaration order" are different requirements — the second is the one the language enforces.

Note. Point 2 is undefined behaviour, so the value you get may differ from your neighbour's. That is not a flaw in the exercise: it is what undefined behaviour means, and it is worth seeing once.

C5 — = default and = delete, tested [L2 Apply]

Task. Three small things:

  1. Write a Point struct with int x; int y;, and confirm Point p{1, 2}; works. Then add Point() = default; to it and confirm brace initialisation still works. Then replace that line with Point() {} and show that it does not.
  2. Write a class whose copy constructor is = delete, and quote the error a copy produces.
  3. Write a class with an explicit single-argument constructor, and quote the error that copy-initialisation produces.

Success looks like. Three results, each with the exact message, and a written paragraph explaining the difference between (1)'s two variants — user-declared versus user-provided, under the C++17 rules this course uses.

Full credit for (1) includes saying whether the second variant still satisfies the definition of an aggregate, and what that has to do with brace initialisation.


Challenge [L4 Analyze] · [L5 Design]

X1 — a type with const members, and where that hurts [L5 Design]

Task. Design LoanRecord: the member's name, the item's identifier, and the day the loan started — all of them settled at construction time and never changed. All three members const.

  1. Write the class, with a constructor that establishes the invariant, and a dueDay() operation that derives the return date from the start day.
  2. Now try to use it the way a library actually would: std::vector<LoanRecord> loans;, then push_back, then erase one.
  3. Write what happens, why it happens, and which single change to the design makes the vector work again.

Success looks like. The class; a compile error quoted exactly; a correct explanation (assignability, and which of the special members const members take away); and a considered fix — not necessarily "remove const from one member", which is one of at least three defensible answers.

How you will be judged. On the reasoning in (3). This is the module's deepest design tension: immutability is good, and it costs you the operations that containers need. Being able to say which cost you are paying, and why you chose to, is the skill.

X2 — the class that owns something [L4 Analyze]

Task. Take Example 5 from Worked examples:

class Catalogue {
public:
    Catalogue() : books_(new Book[8]), count_(0), capacity_(8) {}
    bool add(const Book& book);
private:
    Book* books_;
    int count_;
    int capacity_;
};

Write out, in full, what this class needs and what breaks without each piece:

  1. the destructor — what it must do, and what happens without it;
  2. the copy constructor — what the compiler's version does, and the exact failure it produces when both copies are destroyed;
  3. the copy assignment operator — the same, plus the one extra problem an assignment has that a copy construction does not;
  4. a fourth thing that goes wrong if the constructor's new throws, or if add throws after count_ has been incremented.

Success looks like. Four findings, each naming the mechanism and the resulting symptom (leak, double free, lost resource, inconsistent count), and one sentence each on what you would rather do instead of writing any of them.

Full credit includes naming the ordering problem in (3) that (2) does not have. It is the reason the copy-and-swap idiom exists, and Module 10 is where it lives.

X3 — make the compiler's own functions correct again [L5 Design]

Task. X2's class needs four hand-written special members. Remove the need for all four:

  1. Change the representation so that the class owns nothing raw — one member type change does it, and it is in the standard library.
  2. Delete the destructor, the copy constructor and the copy assignment operator you concluded were needed. Compile.
  3. Write what disappeared, and why the generated versions are now correct.
  4. Then name the class that still genuinely needs a hand-written destructor, even after this change — because it owns something the standard library does not cover.

Success looks like. A class with no hand-written special members at all, that builds and runs; a paragraph naming the principle (let a member own the resource, and its own destructor becomes the whole story); and for (4) one honest example — an open file, a lock, a handle — with the sentence "this is why std:: provides a wrapper for it".

How you will be judged. This is the module's design destination. You are not being asked to know the rule's name (Module 11 gives it one); you are being asked to reach the conclusion by removing code and watching the class get safer as it shrinks.


Stretch (optional)

  • S1. Build examples/module-03/01-lifecycle-trace/main.cpp with -std=c++14 and compare the trace with the C++17 run. Both are valid C++; explain why this program is the wrong one to demonstrate the difference, and then modify it so that it is the right one. (Lab 3's makeItem is the answer — write your own version before looking.)
  • S2. Add a move constructor to your C1 class (the syntax is Locker(Locker&& other), and you do not need to understand it yet) that prints "moved". Predict how many "moved" lines a std::vector<Locker> produces while growing from 1 to 9 elements. Then run it, and write down the ratio. Module 10 explains the number.
  • S3. Take a class from an earlier course or your own work that has a destructor, and check whether it also has a copy constructor and a copy assignment operator. Do not fix anything — write one paragraph on what would happen if the class were copied.

Solutions

Exercise Reference
All warm-ups The traces in examples/module-03/01-lifecycle-trace/main.cpp and 02-initialiser-lists/main.cpp
C3, C5 examples/module-03/03-copies-and-temporaries/main.cpp — the commented-out failures are C5's answers
C4 examples/module-03/02-initialiser-lists/main.cpp — the Rectangle class, with the order explained in a comment
C2 examples/labs/lab-03/solution/main.cpp plus its prediction.md, which is the honest version of this exercise: two predictions wrong, and the reasons given
X2 examples/labs/lab-04/solution/justification.md and Module 10's blueprints

C1 has no published solution on purpose: it is the smallest class in the module and a published version would be read rather than written.

X1 and X3 have no single right answer. Compare your reasoning with Module 10's opening problem, which is exactly X3 approached from the other direction.


Next: Lab 3 — instrument the running system, and hand in a prediction. · All modules