Skip to content

Module 7 — Exercises

Every exercise states its level, its task, and the observable result that counts as success. If you cannot tell whether you succeeded without reading a solution, the exercise is defective — tell the course maintainers.

Solutions point at programs you have already been given. Attempt each exercise before you look: recognising an answer later is not the same as producing it now.


Warm-up [L1 Understand]

W1 — which calls dispatch?

Task. For each call, say whether it is statically or dynamically bound, and name the function that runs. Assume every base function below is virtual unless stated.

struct Base { virtual void f(); virtual void g() const; void h(); };
struct Derived : Base { void f() override; void g() override; void h(); };

Derived d;  Base& b = d;  Base* p = &d;  const Base cb = d;

1. d.f();            2. b.f();            3. p->f();
4. d.g();            5. b.g();            6. cb.g();
7. d.h();            8. b.h();            9. Base{v}.f();       // v is a Derived

Success looks like. Nine verdicts, each with the deciding reason — virtual present or not, handle or object, and for line 9 the word slicing.

Stuck? Concepts §1 and §2.


W2 — name the mechanism

Task. Match each symptom to the mechanism, and say whether a compiler message is produced:

Symptom Candidate mechanisms
A copy loses data that the original had (a) missing virtual
The base version runs through a base reference (b) slicing
A buffer owned by a derived object is never freed (c) non-virtual destructor
override fails to compile (d) signature mismatch
A virtual call in a constructor prints the base's text (e) incomplete object
A derived print(std::string) breaks print(int) call sites (f) name hiding

Success looks like. Six mappings, and for each one yes/no on the diagnostic — three of them are silent, and knowing which is the point.

Stuck? Misconceptions and mistakes.


W3 — predict four virtual calls

Task. Without running examples/module-07/05-virtual-calls-in-constructors/main.cpp, write the four lines that describe() prints for one Derived object in these positions: inside Base()'s constructor, inside Derived()'s constructor, inside ~Derived(), inside ~Base(). Then run it and compare.

Success looks like. Four lines, written before the run, plus the reason for each transition in one clause. A wrong prediction corrected in your own words is worth more than a right one with no reason.

Stuck? Concepts §7.


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

C1 — make the fleet bill correctly [L2 Apply]

Task. Take the non-virtual half of examples/module-07/01-virtual-or-not/main.cpp, and make the fleet total correct without changing any printed text. You will need exactly one keyword, and you should also decide whether any other function in that hierarchy needs it.

Success looks like. The non-virtual totals become 780000 and every per-invoice figure agrees with the virtual version's. diff the two halves of the program's output and be able to explain any remaining difference.

Stuck? Concepts §1.


C2 — three defects, two of them silent [L3 Modify]

Task. This program compiles, runs, and is wrong in three ways. Find all three before you change a line.

class Account {
public:
    Account(long long balance) : balance_(balance) {}
    long long interest() const { return 0; }                  // (1)
    virtual std::string kind() const { return "account"; }
private:
    long long balance_;
};

class Savings : public Account {
public:
    Savings(long long balance, int rate) : Account(balance), rate_(rate) {}
    long long interest() const { return rate_; }              // (2)
    std::string kind() const override { return "savings"; }
private:
    int rate_;
};

long long totalInterest(std::vector<Account> accounts) {      // (3)
    long long total = 0;
    for (Account account : accounts) total += account.interest();
    return total;
}

int main() {
    std::vector<Account> accounts{Savings(1000000, 50000), Account(2000000)};
    std::cout << totalInterest(accounts) << '\n';
}

Success looks like. Three named defects, each with the line that would fix it, and a predicted output before the fix with a one-sentence reason. Requirement: your fix for (3) must hold handles, and you must say what new question that introduces about lifetime.

Stuck? Misconceptions and mistakes, mistakes 1 and 5; Concepts §6 for the container rule.


C3 — the declared-type test on paper [L4 Analyze]

Task. Take examples/module-07/04-slicing/main.cpp and perform the declared-type test for every call in it: static type, virtual, handle form. Six or seven lines of output, one per call site.

Success looks like. A completed table where the row for byValue shows handle: object and the consequence words no derived part to dispatch to — and where you can point at the two rows that differ only in handle form and explain why their results differ.

Stuck? Debugging.


C4 — the missing virtual [L4 Analyze]

Task. A colleague's hierarchy prints base (unknown) for every object in a report loop. You have one file and two minutes. Below is what the loop sees; find the defect, name the line, and write the diagnosis as a comment you would leave in the code.

class Report {
public:
    virtual ~Report() = default;
    std::string summary() const { return "base (unknown)"; }
    virtual std::string title() const { return "report"; }
};

class SalesReport : public Report {
public:
    std::string summary() const { return "sales: 42 invoices"; }
    std::string title() const override { return "sales"; }
};

void printAll(const std::vector<Report*>& reports) {
    for (const Report* report : reports) {
        std::cout << report->summary() << '\n';       // prints "base (unknown)" every time
    }
}

Success looks like. The word virtual on the right line, an explanation that mentions the static type of the expression, and a note saying why title() was unaffected — which is the half of the diagnosis that proves you understood rather than guessed.

Stuck? Debugging's declared-type test.


Challenge [L5 Design] · [L7 Refactor]

X1 — one notification interface, many channels [L5 Design]

Task. A university system must notify students by email, SMS and push notification. Design it so that adding a channel does not edit existing code, and so that a queue of notifications can be walked without slicing.

Deliver: (a) the interface, with the destructor rule applied; (b) a queue type and the ownership decision behind it (values, pointers, or smart pointers — choose and justify); (c) a Channel implementation that fails (SMS rejects an over-length message), with the failure reported rather than swallowed; (d) one paragraph on what a caller must know about the objects' lifetimes; (e) the change you would have to make to add a channel that sends by post, and why it is or is not a one-class change.

Success looks like. The queue holds no copies of channel objects; the interface is small enough that a fourth channel implements it without stubs; and (d) names a concrete lifetime hazard rather than saying "ownership is important".

How you will be judged. The rubric — (b) and (d) carry most of the marks, because ownership is where polymorphic designs break.


X2 — replace the tag, then measure [L7 Refactor]

Task. Take examples/module-07/06-switch-to-dispatch/main.cpp and go one step further: add a fourth shape to version 1 first (count the edits), then do the conversion to version 2, then add the same shape to version 2 (count the edits). Report both counts.

Then measure the cost you have accepted: print sizeof of every concrete class and of the base; time a loop of one million area() calls in each version; and report whether the difference is visible at all, and what your measurement does not tell you.

Success looks like. Two edit counts, a table of sizes, two timings, and a paragraph that says what the numbers do not show — inlining, vtable lookup, cache effects, and the fact that one implementation's timing is not a guarantee about another's.

Stuck? Concepts §3 for what the sizes mean, and §9 for why "virtual is slow" is not an argument on its own.


X3 — the memo: where not to use virtual [L8 Evaluate]

Task. Write a one-page design memo for a hypothetical team: "When we will not use virtual dispatch." Your reader is a competent C++ programmer who has just finished Module 7 and now wants to make everything virtual.

Cover at least: value types and why they should stay values; hot paths, and what evidence is required before you optimise; the difference between an operation that is expected to vary and one that merely could; and what a class with a virtual function can no longer be (freely copied, sorted by value, held in a std::vector without handles). End with two rules you would enforce in code review, each one greppable.

Success looks like. Four sections, each with a rule and a reason; at least one place where you recommend virtual despite the cost, argued rather than asserted; and no claim about performance that you have not measured or cited.

How you will be judged. Whether a reader who disagrees can follow the reasoning, and whether your rules could actually be applied to a diff.


Stretch (optional)

  • S1 — "What changes if…" you add virtual to every function in a small type and then store a million of them in a std::vector. Measure sizeof and the vector's total memory, and say what the change cost.
  • S2 — "What changes if…" the container in Example 4 held std::reference_wrapper<const Person> instead of const Person*? Which of the three rules it breaks or preserves, and what new syntax it demands.
  • S3 — "What changes if…" Base::describe() in Example 5 were non-pure and non-virtual? Predict all four lines of the trace, then run it and explain any difference from the version with virtual.

Solutions

Exercise Reference
W1, W3, S3 examples/module-07/05-virtual-calls-in-constructors/main.cpp — the four-answer trace, with the reason for each line
W2 Misconceptions, Part 1 — the six mechanisms, with the three silent ones marked
C1 The two hierarchies in examples/module-07/01-virtual-or-not/main.cpp; the correct total is 780000
C2, C3 examples/module-07/04-slicing/main.cpp — slices measured with sizeof, and a container of pointers that fixes them
C4 Misconceptions, Mistake 1 — the same defect with a different surface
X1 No published answer: the ownership decision is the exercise. Module 11 is the destination, but a raw-pointer answer with a stated lifetime rule is acceptable here if the rule is honest
X2 examples/module-07/06-switch-to-dispatch/main.cpp — version 1 and version 2 side by side, including the -Wswitch caveat
X3 Concepts §9 is the starting list, not the memo. The judgement is yours

X1 and X3 have no published answer on purpose. Compare with the rubric after writing your own.


Next: Lab 7 — the motor pool gets an interface, and you reproduce slicing on purpose before you fix it. · All modules