Skip to content

Module 6 — Lab 6: a family of lendable items

Levels: [L3 Modify → L5 Design] · Extends: Lab 5 · Time: 5–7 hours


The scenario

The desk has been running on one kind of item. This term the catalogue takes journals and DVDs as well, and the acquisitions clerk has started asking questions the system cannot answer: "what is one item's loan period?" depends on the kind — books go out for three weeks, journals for two days, DVDs for a week. The Book-only design cannot express that, and copying the identifier/title/on-shelf trio three times is what Lab 5's operators just made unnecessary.

Your task is to grow the item family without letting the shelf rule, or the identity rule, become something a caller can break.

The deliverable includes a prediction. Before you run your hierarchy for the first time, you write down the exact construction and destruction order of one three-level object and hand that list in with the program. The list is marked, and a wrong list with a corrected explanation beside it is worth more than a right list with no evidence that you checked.


What you start from

  • Your Lab 5 solution: Book with ==, < and <<, a static identifier allocator, and a Catalogue that owns its items.
  • Your Lab 4 solution behind it, for the encapsulation rules you must not lose — this lab adds a hierarchy underneath an interface that already holds invariants.

If your Lab 5 solution has a mutable accessor, fix that first. A hierarchy multiplies the damage: an invariant that a derived class can break is now breakable from two places instead of one.


Requirements

  1. A base class holding only what is genuinely common to every kind of item — identifier, title, and whether it is on the shelf — with private data and a deliberate decision about which members, if any, are protected. (Interacts with Module 4: the shelf flag is an invariant, so nothing outside the class may set it; derived classes request a change through a base operation that can refuse.)
  2. Three derived classes — Book, Journal, Dvd — each adding only what it needs, each with a public inheritance relationship, and each with a loanDays() answering the clerk's question. State in a comment, for each, whether loanDays() hides or overrides the base's — and why it is not yet a dispatch.
  3. A three-level hierarchy somewhere, with a Tracer-style member in at least two classes, so that the construction and destruction order is observable. Journal → BoundJournal → ArchiveVolume, or whatever fits your design.
  4. The predicted trace, written before the first run: the exact order of the lines your program will print for one object of the deepest class, both constructing and destroying.
  5. A relationship audit, one row per public operation of your base class: operation · is it true of all three kinds · what a caller could do with it that breaks an invariant. Every "breaks" row must be answered — by narrowing the operation, by moving it out of the base, or by a documented reason why the risk is acceptable.
  6. One composition decision, justified in writing. Take one candidate relationship from your design (a Dvd that "is a" MediaItem, a Catalogue that "is a" std::vector, a ShelfLocation you were tempted to inherit) and replace the inheritance with a member. Deliver the before/after and two sentences: what the base would have published that the derived type cannot honour, and what the member version hides instead.
  7. It must build with no warnings using the course's compile command: g++ -std=c++17 -Wall -Wextra -Werror.

What to deliver

  • A program that builds the three kinds of item, prints each one's id, title, loanDays() and shelf state, and hands an item to a free function taking const BaseItem&.
  • The predicted trace, with the real trace printed next to it and any difference annotated — this is the lab's evidence requirement, and a run that matched a prediction is still worth recording.
  • The relationship audit, as a Markdown table, one row per base operation.
  • The composition note, with the before/after and the two sentences.

Self-assessment checklist

It builds and runs

  • [ ] Compiles with g++ -std=c++17 -Wall -Wextra -Werror with no warnings
  • [ ] Prints all three kinds with the right loanDays() and shelf state
  • [ ] The free function taking const BaseItem& compiles and its output is what you said it would be — including the part that is not polymorphic

It is correct

  • [ ] Every derived class constructs its base first, in the initialiser list
  • [ ] The predicted trace and the real trace are both present in the deliverable
  • [ ] The shelf flag cannot be set from outside, from a derived class, or through any operation the base publishes
  • [ ] Boundary cases work: an item with an empty title, two items with the same identifier, an item that is already on the shelf
  • [ ] loanDays()'s hiding-versus-overriding status is stated for each class, and the answer matches what the program prints

It is designed

  • [ ] I can state each class's responsibility in one sentence
  • [ ] Every protected member has a written reason for existing
  • [ ] Each relationship is labelled is-a or has-a, and the labels match the code
  • [ ] The base class has no member that is meaningless for one of its derived types, or that member is named in the audit with its cost
  • [ ] The composition note names the operation the base could not honour

It is verified

  • [ ] I predicted the construction order before running, and wrote the prediction down
  • [ ] I reproduced at least one silent failure on purpose — a hidden overload or a duplicated subobject — and recorded what happened
  • [ ] My audit lists attacks that compile, not only attacks that do not

Common ways this lab goes wrong

Symptom Cause What to do
error: no matching function for call to 'BaseItem::BaseItem()' A derived constructor does not name the base, and the base has no default constructor Put the base first in the initialiser list, as you did for members in Module 3
error: 'BaseItem' is an inaccessible base of 'Book' class Book : BaseItem — the mode is missing, and class defaults to private Write : public BaseItem, and never write an inheritance without a mode
One item's loanDays() is always 21 The function is not virtual, so a query through const BaseItem& runs the base's Say so in the comment the lab requires; Module 7 is the fix, and predicting this now is the point
Adding print() to a derived class breaks unrelated call sites Name hiding: one derived print removes every base print using BaseItem::print;, or qualify the call
The audit has rows that start "callers should not…" The audit is being written as an intention rather than a fact Rewrite each row as an attack that compiles, or delete the row and say why none exists
The trace was "obviously" right so it was not written down The deliverable was skipped Re-run from a clean build and write the real order; the prediction's value is that it was made before
The composition note describes the change but not the cost The trade-off was not examined Name the operation the base published that the derived type cannot honour — that sentence is the note

Reference solution

There is no published file for this lab, on purpose: it is your Lab 5 solution with a base class factored out, and the decisions are the work — which members are shared, which are protected, and which relationship you refused. What follows is the part the module's examples do not show together: a base whose protected interface exists for exactly one reason, and the trace prediction written the way it must be written.

// The parts to compare your version against.

class BaseItem {
public:
    virtual ~BaseItem() = default;          // an item may be deleted through a base
                                            // pointer — see Module 7 for why

    int id() const { return id_; }
    const std::string& title() const { return title_; }
    bool onShelf() const { return onShelf_; }

    // Every kind of item lends for a different period. Not virtual yet: this
    // module is about the hierarchy, Module 7 is about the dispatch.
    int loanDays() const { return kDefaultLoanDays; }

protected:
    // Exists so that a derived class can declare a period it supplies itself,
    // and for nothing else. A derived class may not set the shelf flag.
    explicit BaseItem(int id, std::string title)
        : id_(id), title_(std::move(title)) {}

    static constexpr int kDefaultLoanDays = 21;

private:
    int id_;
    std::string title_;
    bool onShelf_ = true;                   // set by the base's own operations only
};

class Journal : public BaseItem {
public:
    Journal(int id, std::string title, int issue)
        : BaseItem(id, std::move(title)), issue_(issue) {}

    int loanDays() const { return 2; }      // HIDES BaseItem::loanDays — not an override
    int issue() const { return issue_; }

private:
    int issue_;
};

And the prediction, written the way the lab requires — as a list of lines, produced before the first run, with the reason for each transition:

Predicting `Journal j(7, "Nature", 12);` inside a scope, with the base's
Tracer member `tag_` declared before any others:

  1. BaseItem::tag_      constructed     the base part is built first
  2. BaseItem            body             base members exist, so the body may use them
  3. Journal::tag_       constructed     this class's members, in declaration order
  4. Journal             body             the object is now complete
  … and on scope exit, the same list read backwards:
  5. Journal             body  destroyed  derived first: it may still use base state
  6. Journal::tag_       destroyed
  7. BaseItem            body  destroyed
  8. BaseItem::tag_      destroyed

Three things to look for when comparing with your version:

  • Which members are protected, and whether each one's sentence is true. In the sketch above, protected protects nothing but the base's constructor: the shelf flag is unreachable and there is no setter, because lending is a base operation that can refuse. If your base hands derived classes a way to set the shelf flag, your audit should say what that costs.
  • Whether the free function's output is honest. Run it with a Journal and look at loanDays(): 21, not 2. That number is not a bug in your code — it is the fact Module 7 exists to fix, and the lab asks you to state it rather than hide it.
  • Whether your audit found the silent failures. The two here — the hidden loanDays() and any duplicated subobject if you used multiple inheritance — have no compiler message. A clean build is not evidence they are absent.

Next: Challenge problems — three open problems, including what a five-level hierarchy costs. · All modules