Skip to content

Module 5 — Lab 5: make the catalogue speak the language

Levels: [L2 Apply → L5 Design] · Extends: Lab 4 · Time: 4–6 hours


The scenario

The library system works. Items and members hold their invariants, and nothing outside the types can put them into a state their author did not intend. What it cannot do is participate: the catalogue's lookup is a hand-written loop, printing a list means a function that every caller must remember, and sorting is impossible. The desk staff have started asking for "the list in identifier order, printed" and there is no operation that means that.

This lab replaces the loops with operations the standard library can drive, and adds the one piece of class-level state the system needs: identifier allocation.

The deliverable includes a failure. You will write a deliberately invalid comparator first, run a sort, and record what happened. That record is marked, and it is worth more than the working version — because "a library algorithm compiled, ran and gave a wrong answer" is a fact best learned on your own machine.


What you start from

  • Your Lab 4 solution: Book and Member with encapsulated interfaces, and a Catalogue that owns its items.
  • Your Lab 3 program as the driver, so the behaviour stays visible.

If your Lab 4 solution still has a mutable accessor or a setter, fix that first — this lab multiplies the damage, because an operator is a public operation and an inconsistent one is a public inconsistency.


Requirements

  1. operator== and operator!= on Book, and the same on Member, consistent with the notion of identity used throughout the system. State identity in one sentence in a comment.
  2. operator< forming a strict weak ordering, verified by sorting a list that contains duplicate identifiers and by the property test in Debugging.
  3. operator<< producing a one-line readable form, used to print the whole catalogue. It must be a free function taking std::ostream&, and it must return the stream.
  4. A static counter allocating identifiers, with either an out-of-class definition or the C++17 inline static form, and a comment explaining the choice and naming the alternative. (Interacts with Module 4: the counter is class-level state, and nothing outside the class may set it.)
  5. Catalogue::operator[] in const and non-const forms — at() returning a reference, at() returning a const reference — with a comment on which one a caller holding a const Catalogue& gets. (Interacts with Module 4: the non-const form must not let a caller break an invariant. If it does, return a value instead and say why.)
  6. The comparator note — a short written record of the invalid comparator: what you wrote, what the sort did, whether it warned you, what happened after you fixed it, and which property was violated.
  7. It must build with no warnings using the course's compile command: g++ -std=c++17 -Wall -Wextra.

What to deliver

  • A program that prints the catalogue in identifier order, finds an item by identifier with a standard algorithm, and allocates identifiers automatically for newly added items.
  • The comparator note, as a Markdown file or a printed session transcript, containing the broken comparator, the observed output, and the fixed version with the difference explained.
  • A comment block in the program listing the four attacks from Lab 4 that used to compile and no longer do.

Self-assessment checklist

It builds and runs

  • [ ] Compiles with g++ -std=c++17 -Wall -Wextra with no warnings
  • [ ] Prints the catalogue in the required order, and the print loop uses << only

It is correct

  • [ ] The sort is verified with duplicate identifiers present, and both survive
  • [ ] The property test (irreflexive, asymmetric, transitive) passes
  • [ ] std::find uses the same notion of identity as std::sort and printing
  • [ ] Boundary cases work: an empty catalogue, one item, two identical identifiers
  • [ ] The identifier counter never hands out the same number twice, including after a copy — if copying an item allocates a new identifier, that is stated

It is designed

  • [ ] I can state each class's identity rule in one sentence
  • [ ] operator<< needs no friend, or its friend is justified in one line
  • [ ] The static counter cannot be set from outside the class
  • [ ] operator[]'s two forms are consistent with Module 4's encapsulation rules, or the non-const form was refused with a reason

It is verified

  • [ ] I predicted the sort's output before running it, including for duplicates
  • [ ] The broken comparator was run, and its output recorded rather than described
  • [ ] I explain the violated property by name, not by "it is wrong"

Common ways this lab goes wrong

Symptom Cause What to do
The sort produces a different order on different runs the comparator is not a strict weak ordering property test, then compare the field that defines the order and nothing else
std::find never finds the item that is visibly there == and < define identity differently pick one, write the sentence, make all three use it
The program links but the counter is always 0 the static member is declared and never defined add the definition, or make it inline static
Printing produces 0x7ffd… for an item operator<< was never found, so the pointer overload was used the type is being printed through a pointer; dereference it, and check the operator's signature
The catalogue cannot be sorted because it cannot be copied the copy constructor was deleted, or it holds a pointer Module 10 and 11 are the answers; for now sort a vector of identifiers
Adding operator< broke the print loop using was forgotten and a name is hidden — or the two operators were written in different units check both are visible, then check consistency
The comparator note says "it did not work" the observation was not recorded rerun the broken version and paste the output; the exercise is the evidence

Reference solution

There is no published file for this lab, on purpose: it is your Lab 4 solution with four operations added, and the decisions are the work. What follows is the part none of the module's examples show together — identifier allocation, the const pair, and printing a whole collection.

// The parts to compare your version against.
class Book {
public:
    explicit Book(std::string title) : id_(nextId_++), title_(std::move(title)) {}
    Book(int id, std::string title) : id_(id), title_(std::move(title)) {}

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

    // Identity: two books are the same book when they have the same identifier.
    bool operator==(const Book& other) const { return id_ == other.id_; }
    bool operator!=(const Book& other) const { return !(*this == other); }
    bool operator<(const Book& other) const  { return id_ < other.id_; }

private:
    static int nextId_;             // declared here…
    int id_;
    std::string title_;
};
// …defined once, here. `inline static` above would remove this line and is the
// C++17 preference; the classic form is shown because the link error it prevents
// is this module's most common mistake.
int Book::nextId_ = 1000;

std::ostream& operator<<(std::ostream& out, const Book& book) {
    return out << '#' << book.id() << "  " << book.title();
}

class Catalogue {
public:
    bool add(const Book& book);

    // Read-only access for a caller holding a `const Catalogue&`.
    const Book& at(std::size_t index) const { return items_.at(index); }

    // Writable access — and the question this lab asks: does handing out a mutable
    // reference let a caller break the catalogue's invariant? If yes, return a copy
    // or remove this overload, and say which in a comment.
    Book& at(std::size_t index) { return items_.at(index); }

    std::size_t size() const { return items_.size(); }

    void sortByIdentifier() { std::sort(items_.begin(), items_.end()); }

private:
    std::vector<Book> items_;
};

std::ostream& operator<<(std::ostream& out, const Catalogue& catalogue) {
    for (std::size_t i = 0; i < catalogue.size(); ++i) {
        out << catalogue.at(i) << '\n';       // const overload: no copy
    }
    return out;
}

Three things to look for when comparing with your version:

  • Which notion of identity you chose, and whether all three operations use it. If your == compares titles and your < compares identifiers, the lab's requirement 1 is not met, whatever the program prints.
  • Whether Book& at(std::size_t) survives your own attack test. Write the line that breaks an invariant through it. If one compiles, either delete the overload or narrow what it returns.
  • What your comparator note records. "The sort gave the wrong order" is not an observation; the pasted output and the violated property's name are.

Next: Challenge problems — three open problems, including whether an operator should exist at all. · All modules