Skip to content

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:

  1. Repository<T> with operations: add, find (returning something that expresses absence), remove_by_id, size, list_sorted, and a for_each-style traversal. Storage, ordering and the duplicate rule are your decisions, but the repository must not require T to have anything it does not need.
  2. Two record types that satisfy the requirements: the student and the book.
  3. 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.
  4. A free function template that works over any container of records: for example count_if_matching(records, predicate), and one arithmetic template such as a clamp or a maximum_by.
  5. Requirements documentation: the operations T must provide, written down in the repository's own comment, and checked as far as the standard allows with static_assert or a trait.
  6. 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

  1. Repository<T> works for both record types with no changes to the records beyond their identifiers.
  2. Duplicate identifiers are refused, in one place, for both types — with a message naming the identifier.
  3. find expresses absence in a documented way that does not require T to be default-constructible or copyable more than necessary.
  4. list_sorted returns records in identifier order, with a documented tie-break, and does not require the storage itself to be sorted.
  5. remove_by_id refuses an unknown identifier, and the size decreases by exactly one.
  6. The repository works with a record type that is movable but not copyable — demonstrate it, or say what would be needed.
  7. The free function template works on any container with begin/end and on a C array.
  8. The third type's failure is a compile error naming a requirement, and it is captured verbatim.
  9. 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.
  10. No virtual anywhere: this genericity is compile-time.

Constraints

  • Header-only for the templates; a small main per 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 auto in 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:

  1. Write the requirement list for T: the operations and members the repository will use. For each, say which repository operation needs it.
  2. Decide what find returns, given that T may not be default-constructible. Write the two candidates and choose.
  3. 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.
  4. Predict the line counts: the two hand-written repositories versus the template version, including the requirement documentation.

Implementation tasks

  1. Implement Repository<T> with the operations and the requirement comment.
  2. Instantiate it for the two record types, and run the same test cases for each.
  3. Write the free function template and use it on both a std::vector and a C array.
  4. Build the third type and capture the error; compare with your prediction.
  5. Add a trait-based check for the identifier requirement and capture the new, friendlier error.
  6. 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 T must 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:

  1. Which two requirements does Note violate, and which repository operation names each in the error?
  2. Which line of your code triggered the instantiation, and how does the error help a reader find it?
  3. Add a static_assert that turns the identifier requirement into one clear sentence, and paste both errors side by side.
  4. Which requirement would you document instead of checking, and why is checking everything a mistake?

Reflection questions

  1. Which requirement did your find's return type force on T, and how did you avoid a worse one?
  2. The template saved roughly seventy lines of duplicated code. What did it cost in error quality and compile time, as numbers?
  3. 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?
  4. A template with one instantiation is usually a mistake. Which of your templates has two, and what would justify a third?
  5. 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?
  6. At what size of T would you stop using Repository<T> and write a class for one type? Answer with the conditions, not the size.

Completion checklist

  • [ ] The compiling programs build with -Wall -Wextra -Werror and 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
  • [ ] find expresses 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.md records 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