Practice Lab 23 — Generic repository for two record kinds¶
Difficulty: Advanced · Estimated time: 6 hours · Module 12: Templates and Generic Programming
Write one container, use it for two unrelated record types, and find out which of your two types does not satisfy the requirements.
At a glance¶
| Difficulty | Advanced |
| Estimated time | 6 hours |
| Module | Module 12 — Templates and Generic Programming |
| Concepts practised | Function and class templates, requirements, concept-like constraints, template errors, type safety, generic algorithms, static_assert |
| Prerequisite | Practice Lab 22 |
| Class design | given at the type level; the template's requirements are yours to state |
Scenario¶
Two departments keep records: the registrar keeps students (identifier, name, programme, enrolled flag) and the library keeps books (identifier, title, author, on-loan flag). Both need the same four operations: add, find by identifier, remove by identifier, and list sorted by identifier.
Two copies of the same code exist today and have already drifted: the student version refuses duplicate identifiers, and the book version silently overwrites. Nobody noticed for a term.
Your job is one Repository<T> that serves both, with its requirements stated rather than assumed — and
with one required type that does not satisfy them, so you can see what a template error looks like when the
requirement is genuinely missing.
Problem statement¶
Implement:
Repository<T>with operations:add,find(returning something that expresses absence),remove_by_id,size,list_sorted, and afor_each-style traversal. Storage, ordering and the duplicate rule are your decisions, but the repository must not requireTto have anything it does not need.- Two record types that satisfy the requirements: the student and the book.
- A third type that deliberately does not — a record with no identifier — used in a small, separate program, so that the compiler error is captured rather than hidden behind conditional compilation.
- A free function template that works over any container of records: for example
count_if_matching(records, predicate), and one arithmetic template such as aclampor amaximum_by. - Requirements documentation: the operations
Tmust provide, written down in the repository's own comment, and checked as far as the standard allows withstatic_assertor a trait. - A measurement: the cost of using
Repository<T>for both types, in lines of code saved, and the compile time of the template instantiated for two types.
In template-notes.md:
- The requirement list, and which of them your
find's return type forced. - The captured compiler error from the third type, with the line in your code that the error names.
- The two ways to make the error friendlier (a trait, a
static_assert, or a named requirement), and which you chose and why. - The line count: the two hand-written versions versus the template version, and the honest conclusion.
Learning objectives¶
By the end of this lab you will be able to:
- Write a class template and instantiate it for two unrelated types.
- State a template's requirements and make violations fail with a message that helps.
- Read a template error, find the line in your own code that caused it, and fix the requirement rather than the symptom.
- Write a function template over a container and a predicate.
- Judge honestly when a template is worth its cost, including its error messages and compile time.
Functional requirements¶
Repository<T>works for both record types with no changes to the records beyond their identifiers.- Duplicate identifiers are refused, in one place, for both types — with a message naming the identifier.
findexpresses absence in a documented way that does not requireTto be default-constructible or copyable more than necessary.list_sortedreturns records in identifier order, with a documented tie-break, and does not require the storage itself to be sorted.remove_by_idrefuses an unknown identifier, and the size decreases by exactly one.- The repository works with a record type that is movable but not copyable — demonstrate it, or say what would be needed.
- The free function template works on any container with
begin/endand on a C array. - The third type's failure is a compile error naming a requirement, and it is captured verbatim.
- The requirement list in the repository's comment matches what the code actually requires — checked by removing each requirement in turn and seeing the template fail.
- No
virtualanywhere: this genericity is compile-time.
Constraints¶
- Header-only for the templates; a small
mainper record kind, and a separate program for the failing type that is expected not to compile. Document how you build the failing one and how you capture its error. - No templates with fewer than two instantiations: a template used once is a question you should answer in the notes rather than in code.
- No
autoin the repository's public signatures where a named type is clearer. - No warnings in the programs that are supposed to compile.
Starter task¶
In template-notes.md, before coding:
- Write the requirement list for
T: the operations and members the repository will use. For each, say which repository operation needs it. - Decide what
findreturns, given thatTmay not be default-constructible. Write the two candidates and choose. - Predict the compiler error for the third type: which line will it name, and what will it say? Write it down, then capture the real message.
- Predict the line counts: the two hand-written repositories versus the template version, including the requirement documentation.
Implementation tasks¶
- Implement
Repository<T>with the operations and the requirement comment. - Instantiate it for the two record types, and run the same test cases for each.
- Write the free function template and use it on both a
std::vectorand a C array. - Build the third type and capture the error; compare with your prediction.
- Add a trait-based check for the identifier requirement and capture the new, friendlier error.
- Measure the line counts and the compile times, and record both.
Test cases¶
| # | Action | Expected result |
|---|---|---|
| 1 | Add three students, list sorted | Identifier order, documented tie-break |
| 2 | Add a duplicate student identifier | Refused, naming it |
| 3 | The same two cases for books | Identical behaviour — the point of the template |
| 4 | Find a present identifier | The record, or a documented result referring to it |
| 5 | Find an absent identifier | Documented absence, no crash |
| 6 | Remove a present identifier | Size decreases by one; the record is gone |
| 7 | Remove an absent identifier | Refused |
| 8 | list_sorted after removals and additions |
Still sorted; storage order unchanged |
| 9 | A movable-but-not-copyable record | Works, or the notes explain what would be needed |
| 10 | Free function template on std::vector and a C array |
Same result both ways |
| 11 | Build the third type | A compile error; the message is captured and the named line recorded |
| 12 | Remove a requirement from T one at a time |
The template fails each time, at the operation you predicted |
Boundary cases to run deliberately: an empty repository; one record; removing the last record; two records with the same sort key but different identifiers.
Expected behaviour¶
Repository<Student> after 3 adds: 3 records
S-1002 Bilal Ahmed BS Data Science
S-1001 Ayesha Khan BS Data Science
sorted by id: S-1001, S-1002, S-1003
add duplicate S-1001 -> refused: identifier S-1001 already exists
Repository<Book> after 3 adds: 3 records
add duplicate B-2 -> refused: identifier B-2 already exists
find S-9999 -> absent (no default-constructed record was needed)
count_if_matching(students, enrolled) -> 2
count_if_matching(array_of_books, ...) -> 1
third type build:
error: 'struct Note' has no member named 'id'
note: required by Repository<Note>::add
line counts: student version 96, book version 94, template version 118 (one implementation)
Extension challenge¶
Give the repository persistence: save and load for records, working for both types.
Record in template-notes.md:
- The new requirements
Tmust satisfy (a field format, a parse operation, or a trait). - Whether the format belongs to the record or to the repository, and the cost of each choice.
- What happens to the requirement list now — is it still small enough to document in a comment?
- Whether this is the point at which a template becomes the wrong tool, and what you would write instead.
- The change cost: lines, and the effect on both record types.
Debugging challenge¶
The repository is used for a third type and the error is unreadable:
In instantiation of 'void Repository<T>::add(const T&) [with T = Note]':
required from here
error: 'const struct Note' has no member named 'id'
error: no match for 'operator<' (operand types are 'const Note' and 'const Note')
note: candidate: bool operator<(const Note&, const Note&) (deleted)
Answer with evidence:
- Which two requirements does
Noteviolate, and which repository operation names each in the error? - Which line of your code triggered the instantiation, and how does the error help a reader find it?
- Add a
static_assertthat turns the identifier requirement into one clear sentence, and paste both errors side by side. - Which requirement would you document instead of checking, and why is checking everything a mistake?
Reflection questions¶
- Which requirement did your
find's return type force onT, and how did you avoid a worse one? - The template saved roughly seventy lines of duplicated code. What did it cost in error quality and compile time, as numbers?
- Which of your requirements is checked by the compiler, which by a
static_assert, and which only by documentation? Why is that ordering not a failure? - A template with one instantiation is usually a mistake. Which of your templates has two, and what would justify a third?
- The free function template works on a C array and a
std::vector. What is the actual requirement it places on its argument, in one sentence? - At what size of
Twould you stop usingRepository<T>and write a class for one type? Answer with the conditions, not the size.
Completion checklist¶
- [ ] The compiling programs build with
-Wall -Wextra -Werrorand no warnings - [ ] The failing program's error is captured verbatim, with the line it names recorded
- [ ]
Repository<T>works unchanged for both record types - [ ] Duplicate identifiers are refused in one place, for both types
- [ ]
findexpresses absence without requiring default construction - [ ] The free function template works on a container and a C array
- [ ] The requirement list is written in the code and matches what the template actually needs
- [ ] Removing a requirement makes the template fail, as predicted
- [ ] All twelve test cases run, with output recorded
- [ ]
template-notes.mdrecords the line counts and the compile-time measurement - [ ] The debugging challenge was answered, with both errors side by side
Related: Lab index · Module 12 — Templates · Previous: Practice Lab 22 · Next: Practice Lab 24 — Ring buffer with non-type parameters