Project: A Library Catalogue
This final project combines the course into one program: a library catalogue that loads books from a file, answers commands typed on standard input, lends and returns books, searches, reports statistics and saves its state. It uses classes with invariants, containers and algorithms, std::optional, exceptions, std::format and file streams — the everyday toolkit of modern C++.
Commands
| Command | Effect |
|---|---|
list | All books, sorted by title |
search TEXT | Books whose title or author contains the text (case-insensitive) |
lend ID NAME | Lend a book to a member |
return ID | Return a book |
stats | Counts by genre and books on loan |
save FILE | Write the catalogue in the same format it reads |
quit | Stop |
The data file
One book per line: id, title, author, year and genre, separated by |:
TXT books.txt
101|The Pragmatic Programmer|Hunt & Thomas|1999|computing 102|Dune|Frank Herbert|1965|fiction 103|A Brief History of Time|Stephen Hawking|1988|science 104|Clean Code|Robert C. Martin|2008|computing 105|The Left Hand of Darkness|Ursula K. Le Guin|1969|fiction
The program
Below is the complete program, followed by a sample session: the commands typed as input, and everything the program printed.
C++ library.cpp
#include <algorithm> #include <cctype> #include <format> #include <fstream> #include <iostream> #include <map> #include <optional> #include <sstream> #include <stdexcept> #include <string> #include <utility> #include <vector> // ---------- domain types ---------- enum class Genre { computing, fiction, science, other }; Genre parse_genre(const std::string& s) { if (s == "computing") return Genre::computing; if (s == "fiction") return Genre::fiction; if (s == "science") return Genre::science; return Genre::other; } std::string to_string(Genre g) { switch (g) { case Genre::computing: return "computing"; case Genre::fiction: return "fiction"; case Genre::science: return "science"; case Genre::other: return "other"; } return "other"; } class Book { public: Book(int id, std::string title, std::string author, int year, Genre genre) : id_(id), title_(std::move(title)), author_(std::move(author)), year_(year), genre_(genre) { if (id_ <= 0) throw std::invalid_argument("book id must be positive"); if (title_.empty()) throw std::invalid_argument("book needs a title"); } int id() const { return id_; } const std::string& title() const { return title_; } const std::string& author() const { return author_; } int year() const { return year_; } Genre genre() const { return genre_; } const std::optional<std::string>& borrower() const { return borrower_; } void lend_to(const std::string& member) { if (borrower_) throw std::logic_error(std::format("'{}' is already on loan to {}", title_, *borrower_)); borrower_ = member; } void give_back() { if (!borrower_) throw std::logic_error(std::format("'{}' is not on loan", title_)); borrower_.reset(); } private: int id_; std::string title_; std::string author_; int year_; Genre genre_; std::optional<std::string> borrower_; }; // ---------- the catalogue ---------- class Catalogue { public: void add(Book book) { auto id = book.id(); if (!books_.try_emplace(id, std::move(book)).second) { throw std::invalid_argument(std::format("duplicate id {}", id)); } } Book& get(int id) { auto it = books_.find(id); if (it == books_.end()) throw std::out_of_range(std::format("no book with id {}", id)); return it->second; } std::vector<const Book*> sorted_by_title() const { std::vector<const Book*> result; for (const auto& [id, book] : books_) result.push_back(&book); std::ranges::sort(result, {}, [](const Book* b) { return b->title(); }); return result; } std::vector<const Book*> search(const std::string& text) const { std::vector<const Book*> result; for (const auto& [id, book] : books_) { if (contains_ignore_case(book.title(), text) || contains_ignore_case(book.author(), text)) { result.push_back(&book); } } return result; } std::map<Genre, int> count_by_genre() const { std::map<Genre, int> counts; for (const auto& [id, book] : books_) ++counts[book.genre()]; return counts; } int on_loan() const { return static_cast<int>(std::ranges::count_if(books_, [](const auto& entry) { return entry.second.borrower().has_value(); })); } std::size_t size() const { return books_.size(); } void save(std::ostream& out) const { for (const auto& [id, b] : books_) { out << std::format("{}|{}|{}|{}|{}\n", id, b.title(), b.author(), b.year(), to_string(b.genre())); } } private: static bool contains_ignore_case(const std::string& haystack, const std::string& needle) { auto lower = [](std::string s) { for (char& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c))); return s; }; return lower(haystack).find(lower(needle)) != std::string::npos; } std::map<int, Book> books_; // sorted by id }; // ---------- input/output ---------- Catalogue load(const std::string& path) { std::ifstream in{path}; if (!in) throw std::runtime_error("cannot open " + path); Catalogue cat; std::string line; int line_no = 0; while (std::getline(in, line)) { ++line_no; std::vector<std::string> fields; std::istringstream ss{line}; for (std::string f; std::getline(ss, f, '|');) fields.push_back(f); try { if (fields.size() != 5) throw std::invalid_argument("expected 5 fields"); cat.add(Book{std::stoi(fields[0]), fields[1], fields[2], std::stoi(fields[3]), parse_genre(fields[4])}); } catch (const std::exception& e) { std::cerr << std::format("{}:{}: skipped ({})\n", path, line_no, e.what()); } } return cat; } void print_book(const Book& b) { std::cout << std::format("{:>4} {:<27} {:<18} {:>4} {}\n", b.id(), b.title(), b.author(), b.year(), b.borrower() ? "on loan to " + *b.borrower() : "available"); } // ---------- the command loop ---------- int main() { Catalogue cat; try { cat = load("books.txt"); } catch (const std::exception& e) { std::cerr << "fatal: " << e.what() << '\n'; return 1; } std::cout << std::format("loaded {} books\n", cat.size()); std::string line; while (std::cout << "> " && std::getline(std::cin, line)) { std::cout << line << '\n'; // echo the command std::istringstream args{line}; std::string command; args >> command; try { if (command == "quit") { break; } else if (command == "list") { for (const Book* b : cat.sorted_by_title()) print_book(*b); } else if (command == "search") { std::string text; std::getline(args >> std::ws, text); auto found = cat.search(text); if (found.empty()) std::cout << "no matches\n"; for (const Book* b : found) print_book(*b); } else if (command == "lend") { int id = 0; std::string member; if (!(args >> id >> member)) throw std::invalid_argument("usage: lend ID NAME"); Book& b = cat.get(id); b.lend_to(member); std::cout << std::format("'{}' lent to {}\n", b.title(), member); } else if (command == "return") { int id = 0; if (!(args >> id)) throw std::invalid_argument("usage: return ID"); Book& b = cat.get(id); b.give_back(); std::cout << std::format("'{}' returned\n", b.title()); } else if (command == "stats") { for (const auto& [genre, n] : cat.count_by_genre()) { std::cout << std::format("{:<10} {}\n", to_string(genre), n); } std::cout << std::format("{} of {} books on loan\n", cat.on_loan(), cat.size()); } else if (command == "save") { std::string path; if (!(args >> path)) throw std::invalid_argument("usage: save FILE"); std::ofstream out{path}; cat.save(out); std::cout << "saved to " << path << '\n'; } else if (!command.empty()) { std::cout << "unknown command: " << command << '\n'; } } catch (const std::exception& e) { std::cout << "error: " << e.what() << '\n'; } } std::cout << "bye\n"; }
Input
list search the lend 102 sam lend 102 alex lend 999 alex return 104 stats borrow 101 save catalogue-backup.txt quit
Output
loaded 5 books > list 103 A Brief History of Time Stephen Hawking 1988 available 104 Clean Code Robert C. Martin 2008 available 102 Dune Frank Herbert 1965 available 105 The Left Hand of Darkness Ursula K. Le Guin 1969 available 101 The Pragmatic Programmer Hunt & Thomas 1999 available > search the 101 The Pragmatic Programmer Hunt & Thomas 1999 available 105 The Left Hand of Darkness Ursula K. Le Guin 1969 available > lend 102 sam 'Dune' lent to sam > lend 102 alex error: 'Dune' is already on loan to sam > lend 999 alex error: no book with id 999 > return 104 error: 'Clean Code' is not on loan > stats computing 2 fiction 2 science 1 1 of 5 books on loan > borrow 101 unknown command: borrow > save catalogue-backup.txt saved to catalogue-backup.txt > quit bye
What it uses
| Technique | Lessons |
|---|---|
| Classes that enforce their invariants in constructors and member functions | 31–32 |
std::optional for "maybe on loan" | 43 |
enum class with conversion functions | 20 |
std::map, try_emplace, structured bindings | 21 |
| Ranges algorithms with projections and lambdas | 24–26 |
| Exceptions for errors, caught per command so the program keeps running | 42 |
File and string streams; std::format | 44–45 |
Non-owning const Book* results pointing into the catalogue | 27 |
Challenges
- Due dates: record a
std::chrono::sys_daysdue date when lending, and add anoverduecommand (lesson 46). - Members: add a
Memberclass with a borrowing limit, and reject loans beyond it. - Persistence: save who has each book, and load it back.
- Split it up: move
BookandCatalogueinto their own header and source files, and build with CMake (lessons 48–49). - Test it: write tests for
Cataloguewith a framework such as Catch2 or GoogleTest, and run them under-fsanitize=address,undefined.
Where to go next
- Read A Tour of C++ by Bjarne Stroustrup for a compact overview of the whole language, and the C++ Core Guidelines for style.
- Explore libraries: Qt for desktop apps, SFML or raylib for games, Boost for almost everything, fmt and spdlog for formatting and logging.
- Contribute to an open-source C++ project, or build something you care about.
- Practise with the exercises and test yourself with the quiz.