Module 9 — Lab 9: the library becomes a system¶
Levels: [L4 Analyze → L6 Build] · Extends: Lab 8 · Time: 6–8 hours
The scenario¶
The catalogue has grown into a system. There are items, there are members, and there are loans — and the desk has three complaints that are all really one question:
"When a member leaves the university, does their borrowing history go with them?" "The same item appears in two members' lists, and nobody can say which is real." "We cannot print the overdue report because the loan desk and the member register each hold a copy of the other."
Those are ownership questions. The system currently answers them by accident, in whichever direction the headers happen to point. Your task is to answer them deliberately, in a diagram, before you write the code.
The deliverable starts with a drawing. The ownership diagram is handed in with the program, and it is marked: every arrow labelled with the relationship, every multiplicity stated, and every arrow carrying a lifetime sentence. A diagram without lifetimes is a picture of a bug waiting to happen.
What you start from¶
- Your Lab 8 items, with the
LoanPolicyabstraction. examples/module-09/01-three-kinds-of-relationship/main.cpp— the three relationships with counters that prove them.examples/module-09/04-exposing-a-collection-safely/main.cpp— the access forms, including the one that breaks everything.examples/module-09/05-extension-by-composition/main.cpp— a small interface over a container, and the invariant that needs two doors closed.
Requirements¶
- An ownership diagram, in ASCII or Markdown, with:
- every class as a box, its members' forms marked (
value,T*,T&, container); - every relationship labelled composition / aggregation / association / dependency;
- multiplicities (
1,0..1,0..*); - one lifetime sentence per non-owning arrow — "the Library must outlive the Loan because …". (Interacts with Module 6: ownership diamonds and inheritance arrows use different notation on purpose.)
- A
Librarythat composes its items and members — it creates them, keeps them, and destroys them, demonstrable with counters, exactly as Example 1 does. - A
Loanthat borrows, holding non-owning references or pointers to oneMemberand oneItem, with the lifetime requirement written in a comment at the declaration. It must not delete either. - A cycle broken by a forward declaration. A member must be able to say which loan it currently holds, and a loan must know its member. Deliver both headers, the source file that includes them, and a comment naming what the forward declaration does not allow.
- An invariant that spans a collection: a member may hold at most N loans, and the library must be able to answer "how many active loans does this member have?" without scanning every loan in the system. Every way to change the collection must maintain it — list the ways in a comment.
- No door to the internals: no public member returns a non-
constreference to a container, and no caller can add or remove an item without going through the library. Demonstrate that the attack from Example 4 does not compile. - At least one deliberate refactor, reported: something you first wrote as inheritance or as a plain pointer member, and changed after drawing the diagram. Say what the diagram made visible.
- 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: adds items and members to the library; lends an item; refuses a loan beyond the limit; reports active loans per member without scanning; returns an item; prints the library's state; and prints counters proving that the items and members died with the library while nothing borrowed was deleted.
- The ownership diagram, with lifetimes.
- The refactor note (three sentences): what you changed, what the diagram showed, what it cost.
- The interface audit: one line per public operation of
Library, saying what it can change and which invariant it must maintain.
Self-assessment checklist¶
It builds and runs
- [ ] Compiles with
g++ -std=c++17 -Wall -Wextra -Werrorwith no warnings - [ ] A loan can be created, reported and returned, and the member's count follows
- [ ] The loan limit is enforced, and the refusal is visible to the caller
- [ ] Items, members and loans are all destroyed exactly once, and the counters agree
- [ ] Nothing borrowed is deleted by the borrower
It is correct
- [ ] Every non-owning member has a written lifetime requirement
- [ ] The cycle is broken, and the program includes the headers in the documented order
- [ ] The active-loan count is maintained by every operation that can change it — the list is in a comment, and it is complete
- [ ] Boundary cases work: a member with no loans, a loan returned twice, an item already on loan, a member at the limit
- [ ] The failure path is exercised: a loan refused by the limit, and an item requested while already out
It is designed
- [ ] I can say, for every member, whether it owns, borrows or merely uses
- [ ] Every member's form (value / pointer / reference / container) is justified in a comment
- [ ] No public operation hands out a mutable reference to a container
- [ ] The ownership diagram's lifetimes match the constructors and destructors in the code
- [ ] At least one design decision changed because of the diagram
It is verified
- [ ] I ran the counters rather than reasoning that destruction must be correct
- [ ] I attempted the Example 4 attack against my interface, and recorded that it does not compile
- [ ] I predicted the destruction order before running the program
- [ ] The refactor note names what the diagram made visible, not just what changed
Common ways this lab goes wrong¶
| Symptom | Cause | What to do |
|---|---|---|
| Items are deleted twice, or a crash at exit | The Loan deletes the item it borrowed |
A borrower never deletes. Delete in the owner's destructor, or nowhere |
| The member's loan count drifts | A second door changes the loans: a friend, an accessor, or an inherited container |
List every mutation path and close the ones the class cannot police |
error: 'Loan' does not name a type in a header that includes the other header |
The include cycle, with the guard already fired | Forward-declare in the header, include in the source file |
error: field 'loan_' has incomplete type 'Loan' |
A value member declared from a forward declaration | Include the header, or hold a pointer instead |
error: use of deleted function 'Member& Member::operator=(…)' |
A reference member for the current loan | Decide what assignment means: reseat nothing and copy the rest, or keep the class non-assignable deliberately |
| The overdue report prints nothing | The report holds a snapshot taken at construction, not the library | Hold the thing, or name the member for the copy it is |
| The diagram has arrows but no lifetimes | It was drawn as a picture rather than as a design | Add one sentence per non-owning arrow: what keeps it alive |
| The refactor note says "composition is better" | The artefact was written as a slogan | Name the specific defect the diagram exposed, and the line that changed |
Reference solution¶
There is no published file for this lab, on purpose: it is a system you design, and the decisions are the deliverable. What follows is the part the module's examples do not show together — a three-way ownership arrangement with a broken cycle and a collection invariant that survives every mutation path.
// The parts to compare your version against.
// ── member.h ────────────────────────────────────────────────────────────────
#pragma once
#include <string>
#include <vector>
class Loan; // forward declaration: enough for a pointer member
class Member {
public:
explicit Member(std::string name) : name_(std::move(name)) {}
const std::string& name() const { return name_; }
// The invariant lives here, and only here: activeLoanCount() is never
// recomputed, so every operation that changes the vector must maintain it.
std::size_t activeLoanCount() const { return activeLoanCount_; }
std::size_t loanLimit() const { return loanLimit_; }
bool mayBorrowMore() const { return activeLoanCount_ < loanLimit_; }
void noteLoan() { ++activeLoanCount_; } // called by the Library only
void noteReturn() { --activeLoanCount_; } // …and these two are the door
// The current loan, borrowed and never owned. Live only while the Library
// keeps it: the Library owns every Loan it creates and destroys them at exit.
const Loan* currentLoan() const { return currentLoan_; }
void setCurrentLoan(const Loan* loan) { currentLoan_ = loan; }
private:
std::string name_;
std::size_t loanLimit_ = 3;
std::size_t activeLoanCount_ = 0;
const Loan* currentLoan_ = nullptr; // non-owning: never deleted here
};
// ── loan.h ──────────────────────────────────────────────────────────────────
#pragma once
#include <string>
class Item; // both members need only a declaration
class Member;
class Loan {
public:
Loan(std::string id, Member& member, Item& item)
: id_(std::move(id)), member_(member), item_(item) {}
// Member& and Item&: neither can be null, neither can be reseated, and this
// class deletes neither. Both must outlive this Loan — the Library guarantees
// it, because the Library owns the members, the items and the loans.
const std::string& id() const { return id_; }
Member& member() const { return member_; }
Item& item() const { return item_; }
private:
std::string id_;
Member& member_;
Item& item_;
};
And the library, where the invariant's door is closed by making the operations the only way in and out:
class Library {
public:
bool addItem(std::string id, std::string title); // composes: creates and owns
bool addMember(std::string name); // composes: creates and owns
// The only two ways a loan appears or disappears, and therefore the only two
// places the member's active count changes.
bool lend(std::string loanId, Member& member, Item& item);
bool giveBack(const std::string& loanId);
std::size_t activeLoansFor(const Member& member) const; // O(1): the member knows
// Read-only views. No container reference is ever returned.
std::size_t itemCount() const { return items_.size(); }
const Item& itemAt(std::size_t index) const { return items_.at(index); }
void forEachLoan(const std::function<void(const Loan&)>& visit) const;
private:
std::vector<Item> items_; // owned: destroyed with the library
std::vector<Member> members_; // owned: destroyed with the library
std::vector<Loan> loans_; // owned, and each holds borrows into the two above
};
Four things to look for when comparing with your version:
- Whether the lifetimes still hold when the library dies.
loans_is declared last, so it is destroyed first — before the members and items it refers to. If your declarations putloans_beforemembers_, the loans are destroyed after their referred-to objects, which happens to be harmless here (a destructor that only releases references) and would be a real bug the moment aLoandestructor usedmember_. Say which order you chose and why. - Whether the active count can drift. In the sketch,
noteLoan()andnoteReturn()are public, so a caller can desynchronise the count in two lines — exactly the defect Mistake 5 describes. The safer version makes them private and declaresLibraryafriend, or returns the count from the library by scanning. Whichever you chose, the audit must say which operations can change the count. - Whether the cycle is genuinely broken.
loan.hincludes neithermember.hnoritem.h; it declares them. That is only possible because both members are references. If yours is a value member, the include is required in the header and the cycle returns — which is the reason the member form and the include structure are one decision, not two. - Whether the borrowing member's identity is stable. This is the one deliberate trap in
the lab.
std::vectorreallocates when it grows, and reallocation invalidates every pointer and reference to its elements — the elements are moved, and a reference cannot be reseated, so aLoan&recorded before the move keeps pointing at freed storage. It is easy to verify in five lines: rememberconst Member* borrowed = &members[0];, push another element, and compareborrowed == &members[0]afterwards. On this course's toolchain that printsfalse. So if yourLoanstores references into astd::vectorthat can grow, adding a member can dangle every existing loan. Choose deliberately and say so:reserveenough capacity, use astd::deque(no reallocation), index into the container instead of referring to elements, or own members throughstd::unique_ptr(Module 11) so their addresses never change. Finding this without being told is the point of the lab; either way, the refactor note must say which you chose.
Next: Challenge problems — three open problems, including an audit of a design you were handed and an evaluation of "composition over inheritance". · All modules