Skip to content

Module 6 — 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, so that "checking your answer" means comparing your behaviour with a program that was verified on this toolchain. Attempt each exercise before you look.


Warm-up [L1 Understand]

W1 — classify the relationships

Task. For each pair, decide whether it should be inheritance or a member, and say which word decides it (is-a, has-a, is-implemented-using):

  1. Manager / Employee
  2. Catalogue / std::vector<Book>
  3. FixedStack / IntList
  4. Circle / Shape
  5. Car / Engine
  6. SavingsAccount / Account
  7. ProxyLogger / FileLogger (a logger that forwards after filtering)
  8. TeachingAssistant / Student, Tutor

Success looks like. Eight verdicts, each with the deciding word and — for the two that are arguable — one sentence saying what additional fact would change your mind.

Stuck? Concepts §8.


W2 — read a diagram back into code

Task. Each of these is-a / has-a mixtures is stated in words. Draw the UML diagram (boxes with - # +, arrows, and 1..* multiplicities) and label each arrow:

A library has many items. An item has an identifier and a title.
A book is an item and adds an author. A DVD is an item and adds a running time.
A member may borrow many items. Each loan is one member and one item.

Success looks like. One diagram, five boxes, every arrow labelled, and no arrow whose label you cannot defend in a sentence.

Stuck? Concepts §1 has the notation; Module 9 has the multiplicity forms, so a comment saying "multiplicity: Module 9" is a fine answer here.


W3 — predict the trace

Task. Before running anything, write the order of these lines for

class A { Tracer a1_, a2_; };                       // declared a1_ then a2_
class B : public A { Tracer b_; };
class C : public B { Tracer c1_, c2_; };
C object;

in two cases: (a) as written; (b) with c2_ and c1_ swapped in the declaration.

Success looks like. Both orders written down, then checked by adding Tracers to a scratch program and running it. A prediction that was wrong is worth more here than one that was right — write the correction next to it.

Stuck? Concepts §4, or run examples/module-06/03-construction-and-destruction-order/main.cpp and extend it.


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

C1 — build the payroll hierarchy yourself [L2 Apply]

Task. Write, from scratch and without looking, three classes: Employee (id, name, monthly paisa; private data, a protected setMonthlyPaisa, a public describe()), SalariedEmployee (adds a grade; its pay is base + grade × 250000) and Contractor (adds a day rate and a days-per-month count; its pay is the product, and it must be updated when the days change).

Requirements: a main that prints each employee's describe() and pay; a free function taking const Employee&; no virtual anywhere.

Success looks like. It compiles with g++ -std=c++17 -Wall -Wextra -Werror, the three pays are right, and you can point at the one line where the free function's output is not what a polymorphic design would give — and explain why.

Stuck? examples/module-06/01-payroll-hierarchy/main.cpp is the reference; try your own version first, then compare.


C2 — four broken hierarchies [L3 Modify]

Task. Each snippet is broken. Say what the compiler will report before you compile it, then fix each one, then compile to check yourself.

// (a)
class Base { public: explicit Base(int n) : n_(n) {} private: int n_; };
class D : public Base { public: D() {} };

// (b)
class D : Base { };                       // Base has a public default constructor

// (c)
class P { public: void f(int); };
class Q : public P { public: void f(std::string); };
void use(Q& q) { q.f(3); }

// (d)
class Person { public: std::string name() const; };
class Student : public virtual Person { };
class Tutor   : public virtual Person { };
class TA : public Student, public Tutor { };
void use(TA& ta) { std::cout << ta.name(); }

Success looks like. Four predictions, four fixes, four compiles. For (b) and (d) your fix must be a design change with a one-line justification, not just the minimum edit that silences the compiler — for (d), say whether the one shared Person is what you actually wanted for a teaching assistant who is also an employee.

Stuck? Misconceptions and mistakes has the verbatim messages for (a), (b) and (d).


C3 — count the subobjects [L4 Analyze]

Task. For each class below, state how many distinct std::string members an object contains, then verify with a program that renames one and prints the others through every qualified path available:

class Person  { std::string name_; };
class Student : public Person {};
class Tutor   : public Person {};
class TA      : public Student, public Tutor {};
class TA2     : public virtual Student, public virtual Tutor {};   // with virtual bases

Success looks like. Numbers predicted first; a program that prints three names for TA and one shared name for TA2; and a written explanation of which subobject each accessor reaches. Note that TA2 requires virtual on the intermediate bases, not only on the shared one — say why in one line.

Stuck? Concepts §7 and examples/module-06/04-hiding-ambiguity-and-the-diamond/main.cpp.


C4 — audit someone else's hierarchy [L4 Analyze]

Task. Take the BrokenStack / IntList pair from examples/module-06/05-composition-over-inheritance/main.cpp and write a three-column audit: every public operation of IntList, whether it is true of a stack, and what a caller could do with it that breaks the capacity rule. Then do the same audit for one hierarchy of your own from C1 — this is the habit the module is really teaching.

Success looks like. A complete audit (all six IntList operations), at least three attacks that compile, and the sentence "the derived type cannot honour …, therefore this is not an is-a relationship" written about the operation that decides it.

Stuck? Concepts §8.


Challenge [L5 Design] · [L7 Refactor]

X1 — design the catalogue's item family [L5 Design]

Task. The campus catalogue will hold books, journals, DVDs and — from next year — e-resources with a URL and an access expiry. Design the class hierarchy.

Deliver: (a) a UML diagram; (b) the base class's interface with access levels; (c) a written paragraph naming every operation of the base and stating why it is true of all four kinds; (d) one candidate operation that you deliberately did not put in the base, with the reason; (e) the sentence saying whether the hierarchy could survive being flattened to one class with a kind flag, and why you rejected or accepted that.

Success looks like. A base with no member that is meaningless for one of its four derived types — the payroll example's Contractor, built with monthlyPaisa = 0 because a contractor has no monthly salary, is the counter-example you must avoid or justify. (Module 8 replaces that smell with an interface; a design that anticipates it is a better answer than one that does not.)

How you will be judged. The rubric — the reasoning counts as much as the diagram.


X2 — refactor an inheritance design into composition [L7 Refactor]

Task. Find or write a hierarchy in which a derived class inherits an implementation it does not fully honour. (BrokenStack : public IntList is the worked case; a FixedArray : public std::vector<int> of your own is a good second.) Refactor it to composition, preserving every capability its callers actually use.

Deliver: the before and after code; a list of call sites that no longer compile, with the sentence "this call was never valid for the type, because …" for each; the number of public operations before and after; and one paragraph on what your callers lost, if anything.

Success looks like. Every capability preserved, the interface smaller, and at least one call site whose removal you can justify as a bug fix rather than an inconvenience.


X3 — evaluate a five-level hierarchy [L8 Evaluate]

Task. Take a hierarchy with at least four levels — the standard library's stream classes (std::ios_base → std::basic_ios → std::basic_ostream → std::ostream → std::ofstream) count, and so does one of your own. Assess it against three criteria: (i) can every derived class honour every inherited operation; (ii) how deep is the coupling between a change at the root and code at the leaves; (iii) is any level present only to reuse code rather than to express a relationship?

Success looks like. A written assessment with a verdict — keep, flatten, or replace with composition — and, for each criterion, one concrete observation about the hierarchy you chose rather than a general opinion. Name the pull towards "deep is bad" and say where it is wrong.

How you will be judged. Whether someone who disagrees with your verdict can still follow your reasoning, and whether you named what would change your mind.


Stretch (optional)

  • S1 — "What changes if…" the base class has a default constructor: which of this module's errors disappear, and which of them become silent bugs instead?
  • S2 — "What changes if…" you add virtual to Employee::monthlyPay but leave everything else alone? Predict, run, then check your prediction against Module 7's first example.
  • S3 — "What changes if…" SalariedEmployee's constructor initialises grade_ before calling Employee(...)? Write the error, then explain why the language cannot allow it.

Solutions

Exercise Reference
W1, C1 examples/module-06/01-payroll-hierarchy/main.cpp — a base with genuinely shared state, a protected setter, and the honest printout of what is not yet polymorphic
W2 Concepts §1 for the notation; Module 9 for multiplicities
W3 examples/module-06/03-construction-and-destruction-order/main.cpp — the trace, including the warning for a wrong initialiser order
C2 (a), (b) Misconceptions, Part 2, mistakes 1 and 3, with the verbatim messages
C2 (c), (d) examples/module-06/04-hiding-ambiguity-and-the-diamond/main.cpp — both cases, including the diamond that produces no diagnostic
C3 The same file, §3 of its output; §7 of Concepts for virtual inheritance
C4, X2 examples/module-06/05-composition-over-inheritance/main.cpp — the audit's facts are all in its output
X1 No published answer. It is a design problem with more than one defensible answer, and the rubric is the marking scheme
X3 No published answer: the assessment is the exercise, and the standard library hierarchy is one you can read for yourself

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


Next: Lab 6 — the catalogue grows a family of items, and you must predict the construction trace of a three-level hierarchy before you are allowed to run it. · All modules