Skip to content

Practice Lab 2 — Library catalogue in plain structs

Difficulty: Beginner · Estimated time: 3 hours · Module 1: C++ and the OOP Mindset

The step between parallel arrays and classes. You group the fields correctly and then discover that grouping data is only half of what a class does.


At a glance

Difficulty Beginner
Estimated time 3 hours
Module Module 1 — C++ and the OOP Mindset
Concepts practised Structs, grouping related data, free functions over a group, separating identity from behaviour, std::vector and std::string
Prerequisite Practice Lab 1
Class design you choose the structs; the operations are given

Scenario

A reading room keeps a small shelf of books. Volunteers record each book, lend it out, take it back, and print the shelf so a visitor can find something. There is no catalogue number system yet: the title is the identifier.

The room's volunteer coordinator has read enough bad code to ask for one thing: no parallel arrays. If a book's title and its shelf position are two different arrays, somebody will one day sort one and not the other.


Problem statement

Write a program that keeps a shelf of books and supports these operations:

  1. Add a book: title, author, year, shelf position.
  2. Lend a book by title, if it is on the shelf.
  3. Take a book back, if it is currently lent.
  4. Print the shelf, in title order, marking which books are out.
  5. Print what is currently lent, with the borrower's name.
  6. Report how many books are on the shelf and how many are out.

Group each book's data into a struct — the fields that describe one book, in one place. The operations remain free functions that take the shelf, by reference.

Then, in design-notes.md, answer the question this lab exists to raise: where do the rules about lending live, and which function would have to change if the rule changed?


Learning objectives

By the end of this lab you will be able to:

  • Group related data into a struct so that the data for one entity cannot be partly updated.
  • Write operations that operate on a collection of structs, and pass it by reference.
  • Distinguish identity (which book this is) from state (whether it is out, and to whom).
  • Explain why a struct plus free functions is not yet encapsulation, using an example from your own code in which an invariant can be broken from outside.
  • Say what would have to change in your program if a book could be lent to two people at once.

Functional requirements

  1. A shelf holds any number of books, in no particular insertion order.
  2. A new book with a title already on the shelf is refused, with a message — the title is the identifier.
  3. Lending a book that is already out is refused, naming the current borrower.
  4. Lending a book that is not on the shelf is refused, naming the title.
  5. Returning a book that is not out is refused.
  6. The shelf listing is sorted by title, and shows each book's status on one line.
  7. The "out" listing is sorted by title, and shows the borrower.
  8. The counts are always consistent: on the shelf + out == total.
  9. An empty shelf is not an error: both listings print a clear "nothing" line and the counts are zero.

Constraints

  • One source file, compiled with g++ -std=c++17 -Wall -Wextra -Werror main.cpp -o shelf.
  • No classes — struct with public data members, and free functions. (A class is next module; using one now would hide the lesson.)
  • std::vector and std::string are expected: this lab is about grouping, not about memory.
  • Every refusal prints a message to std::cout — not to std::cerr — so that the expected-output comparison in this lab's table works, and so the tests can be scripted.
  • The lending rules must appear in one function each, not duplicated in the menu loop.
  • No warnings.

Starter task

Write the structs and the function declarations only. Then answer in design-notes.md:

  1. Which fields describe what a book is, and which describe what has happened to it? Split your struct accordingly in your notes — you do not have to split it in code.
  2. A volunteer writes shelf.at(0).on_loan = false; in the menu loop without setting the borrower's name. What is now true of the program, and which function should have been called?
  3. Which single value could you derive instead of storing, if you had to? What would that cost on each listing?

Implementation tasks

  1. Define the structs and the shelf.
  2. Write add_book, lend, give_back, print_shelf, print_loans and counts.
  3. Write the menu loop, calling the functions rather than re-implementing their rules.
  4. Run every test case and record the output.
  5. Write design-notes.md with the three starter questions answered and one extra: which invariant can be broken by the person using this program's functions, and how?

Test cases

# Action Expected result
1 Add three books, list the shelf Three lines, in title order
2 Add a duplicate title Refused, naming the title
3 Lend a book that is on the shelf Accepted; the borrower is recorded
4 Lend the same book again Refused, naming the current borrower
5 Lend a title that is not on the shelf Refused, naming the title
6 Return a book that is out Accepted; the borrower is cleared
7 Return a book that is not out Refused
8 List the shelf after lending The book is marked out, the rest unchanged
9 Counts after two lends shelf 1, out 2, total 3
10 Counts on an empty shelf, both listings nothing on the shelf / nothing is out, counts 0 0 0

Boundary cases to run deliberately: a book lent and returned twice; a title that differs only in case (compilers and Compilers) — decide whether they are the same book, and write the decision down.


Expected behaviour

$ ./shelf
1) add  2) lend  3) return  4) list  5) out  6) counts  7) quit
> 1
title: Compilers
author: Aho et al.
year: 2006
shelf position: A3
added.
> 2
title: Compilers
borrower: Bilal
lent.
> 4
* Compilers  (Aho et al., 2006)  shelf A3  [OUT: Bilal]
  Thinking in Systems  (Meadows, 2008)  shelf B1  [on shelf]
> 6
shelf 1, out 1, total 2
> 7

Extension challenge

Add a second copy of a title, so the same title can appear twice and two people can borrow it.

Notice what breaks: the title is no longer an identifier, find-by-title is no longer unique, and the concept of "a copy" has appeared. Record in design-notes.md:

  • The fields you had to add, and to which struct.
  • The functions whose meaning changed, and how.
  • The identifier you chose for a copy, and why.
  • Which of the six operations now needs two identifiers instead of one.

This is the change that will make you want a Copy class in a later lab. Write down the sentence that would describe it.


Debugging challenge

Two volunteers edit the shelf at the same time and the counts stop adding up:

shelf 2, out 1, total 4

Find the cause in your own code. You have three places that could be wrong — the lend, the return, and one of your own choosing. Answer:

  1. Which single operation can produce this line, and what does it do that no other operation does?
  2. Which invariant is shelf + out == total an instance of, and where is it not enforced?
  3. What would have to change so that this line cannot be printed at all?

Do not patch the counts function to "fix" the arithmetic. The count is a symptom.


Reflection questions

  1. Which struct fields would you want to make unreachable from outside, and why?
  2. lend and give_back are the only operations that change the state fields. Is that enough to make the invariant safe? Why not?
  3. A volunteer asks for a "sort by author" listing. How many functions does that touch, and where should the ordering rule live?
  4. If a book had a status that could be on shelf, out, reserved or lost, what would you store, and what would you worry about?
  5. Name the objects in this problem, and for each one say which of your free functions would become its operations.
  6. Which of your functions takes the shelf and something else, and would be better as a function of one object?

Completion checklist

  • [ ] It compiles with -Wall -Wextra -Werror and no warnings
  • [ ] No parallel arrays anywhere — one struct per book
  • [ ] All ten test cases run, with output recorded
  • [ ] Boundary cases run, including the two books whose titles differ only in case
  • [ ] Both lending rules live in one function each, and the menu loop calls them
  • [ ] design-notes.md answers all three starter questions and names one breakable invariant
  • [ ] The extension challenge was attempted and its findings recorded
  • [ ] The debugging challenge was reproduced and explained before being fixed
  • [ ] I can say which of my functions would become a method, and on which object

Related: Lab index · Module 1 — C++ and the OOP Mindset · Previous: Practice Lab 1 · Next: Practice Lab 3 — Rectangle and geometry model