Module 12 — Lab 12: one repository, several kinds of record¶
Levels: [L3 Modify → L6 Build] · Extends: Lab 11 · Time: 3–4 hours
The scenario¶
By Lab 11 your library system had three containers that were doing the same job: a catalogue of items, a register of members, and a set of loans. Each had its own add, its own find, its own size, and its own copy of the same loop. Two bugs found in one of them had already been fixed twice — and a fourth record kind is coming, because the campus wants to track equipment loans in the same system.
You are going to replace all three with one component, Repository<T>, and you are going to write
down what it requires of the type it stores. The requirements are the deliverable as much as the
code is: a generic component whose requirements exist only in your head has not been generalised, it
has been tested twice.
What you start from¶
Your Lab 11 program, and these three containers, which are real and correct — and all the same shape:
class Catalogue {
public:
void add(const Item& item);
const Item* find(int id) const;
std::size_t size() const;
void forEach(const std::function<void(const Item&)>& visit) const;
private:
std::vector<Item> items_;
};
class MemberRegister { /* identical, with Member */ };
class LoanLedger { /* identical, with Loan, plus a count of overdue loans */ };
Read them before you replace them. The differences are the interesting part: what the element type
appears in, and the one extra operation LoanLedger has.
Requirements¶
-
One
Repository<T>replacing at least two of the three containers. The third may stay if you can justify it — and justifying it is part of the work. -
The operations: add, find by a key or a predicate, remove, size, and iteration. The element type should appear only where it must (interacts with Module 4: the repository's own invariant must still be protected, and a container reference handed out is still a broken invariant).
-
At least one operation that depends on the element type's requirements — ordering, equality, or a key accessor — with those requirements documented in the header, next to the declaration. Say what
Tmust provide and why each item is required. -
A
static_assertrejecting at least one unsuitable type with a readable message. On this toolchain, the message appears as one line:
error: static assertion failed: Repository<T>::sortedById() requires T to have an int member named id
If the requirement is harder to express than a trait can check, say so in the header and explain what the failure looks like instead.
-
The definition visible where it is instantiated — no link errors. If you use explicit instantiation instead of a header-only definition, say why, and note what it costs.
-
Two instantiations in use in the program, for two genuinely different record types, with a note on what the compiler generated for each — including the
sizeofof each instantiation and one sentence on why they differ, or why they do not. -
A recorded error reduction. Use the repository with a type that does not satisfy its requirements, capture the whole diagnostic, then reduce it to at most five lines, quote the line that names the cause, and write one sentence saying what the compiler was telling you about your type. This is a required deliverable, not a debugging footnote. (It is the lab's one deliberate failure, and the record is what makes the next storm manageable.)
-
It must build with no warnings using the course's compile command:
What to deliver¶
- The generic repository in use for two record types, with the three containers replaced.
- The documented requirement list, in the header, next to the template it constrains.
- The
static_assert, and its message quoted from a build where it fired. - The recorded template error, reduced to five lines, with the causal line quoted.
- A note on the two instantiations: what was generated, and what it cost.
- A sentence on the container you did not replace, and why.
Self-assessment checklist¶
It builds and runs
- [ ] Compiles with
g++ -std=c++17 -Wall -Wextra -Werrorwith no warnings - [ ] Both instantiations run, and produce the same results the three hand-written containers did
- [ ] No link errors, and I can explain why definition visibility matters
It is correct
- [ ] Boundary cases work: an empty repository, one element, duplicates, a missing key
- [ ] The repository's invariant is protected from outside mutation
- [ ] Removal and iteration behave correctly when they happen in the same operation
- [ ] A type that does not meet the requirements fails with a message I can read
It is designed
- [ ] I can state the repository's responsibility in one sentence — if it needs two, I have two components
- [ ] The element type appears only where it must in the interface
- [ ] The requirements are written down, and each one is required by something in the body
It is verified
- [ ] I instantiated it with two unrelated types, not two similar ones
- [ ] I forced the requirement failure on purpose, captured it, and reduced it
- [ ] I checked the removal and iteration paths, not only add and find
- [ ] I said what generalising cost: compile time, code size, error complexity
It is explained
- [ ] I can say why a template was the right tool here, and name the case where a virtual interface would have been right instead
- [ ] I can explain what the compiler generated for each instantiation
Common ways this lab goes wrong¶
| Symptom | Cause |
|---|---|
undefined reference to 'Repository<Book>::add(Book const&)', or a linker message naming no symbol at all |
The definition is hidden in a .cpp. Move it to the header, or instantiate explicitly |
| The repository compiles for the first record type and fails for the second | A requirement that the first type satisfied incidentally. Write it down, then constrain or generalise |
| The same add/find loop still exists three times | The third container was copied instead of replaced, or the operation was kept on the class rather than moved into the repository |
where() returns std::vector<T> and the caller now has copies |
A query returning values copies; return references, indices, or iterate with a callback |
| Hundreds of lines of error, none naming a file of mine | The reduction step was skipped. Read the last line, find your own filename in the required from chain, reproduce in five lines |
error: no matching function for call to 'Repository<Book>::add(Member&)' |
The two instantiations were mixed up: a Repository<Book> was given a Member. Each instantiation is a different type with no conversion |
| The lab takes twice the estimate | You are generalising all three containers and inventing a new record type. Two containers and two types is the requirement |
Reference solution¶
No solution file is published for this lab. It is one repository type that must serve several record kinds, and the decisions are the work: what the type requires of a record, where a requirement is discovered only by instantiating the template a second time, and what the compiler's error tells you when it is not satisfied. A published file would remove exactly the step that teaches this module.
The model code is in examples/module-12/, one program per idea, each already walked
through on the concepts page where it is used:
| Program | What it models |
|---|---|
01-function-templates |
Function templates: one definition, many types |
02-class-templates |
Class templates: one class, many element types |
03-non-type-parameters |
A template parameter that is a value rather than a type |
04-specialisation-and-traits |
Specialisation, type traits, and if constexpr |
05-generic-repository |
A generic repository — the class this lab's catalogue needs |
Compare after your own version builds and runs — the reduced error message is the part worth comparing.
Next: Challenge problems — three open problems, including arguing that a component should not be a template. · Previous: Lab 11 · Then: Module 13 — Exception Handling — Lab 13 makes failure part of the design, and assumes the resources in your system are owned by types.