Project: Library Manager
Time to bring the whole course together. In this project you will build a console application for a small school or college library. Librarians can add books, lend them to members, take them back, search the catalogue and see statistics, and everything is saved to a file so nothing is lost when the program closes.
You will use most of what you have learned: a struct and a class, std::map, std::optional, std::expected, ranges and views, std::format alignment, file streams, input validation and a command loop. Build it step by step, compiling and running after each step, exactly as you would in a real job.
Step 1: plan the program
Before writing code, decide what the program must do and how the data will look.
Features:
- Add a book with a title and an author; the program gives it an ID number.
- List all books with their status.
- Lend a book to a member, refusing if it is already out or the ID is wrong.
- Accept a returned book.
- Search titles and authors, ignoring upper and lower case.
- Show how many books there are and how many are on loan.
- Save everything to
library.txton exit and load it on start.
Data design:
- A
Bookholds an ID, a title, an author and an optional borrower. "Optional" is exactly whatstd::optional<std::string>expresses: either a member's name or nothing. - The
Libraryclass keeps books in astd::map<int, Book>keyed by ID, so lookups by ID are fast and listings come out sorted by ID. - Operations that can fail, like lending, return
std::expected<void, std::string>: success with no value, or a message explaining the problem.
Put all the code in one file, library.cpp, in the order the steps present it, and compile with g++ -std=c++23 -Wall -Wextra library.cpp -o library (add -lstdc++exp with MinGW GCC 14 or 15).
Step 2: the Book type and helpers
Start with the headers, the Book struct and three small helper functions.
// library.cpp: a small library manager in C++23
#include <algorithm>
#include <cctype>
#include <expected>
#include <format>
#include <fstream>
#include <iostream>
#include <limits>
#include <map>
#include <optional>
#include <print>
#include <ranges>
#include <sstream>
#include <string>
#include <string_view>
#include <vector>
struct Book {
int id = 0;
std::string title;
std::string author;
std::optional<std::string> borrower; // empty while the book is on the shelf
bool available() const { return !borrower.has_value(); }
};
std::string to_lower(std::string_view text) {
std::string result(text);
for (char& c : result) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
return result;
}
void print_header() {
std::println("{:<5}{:<16}{:<22}{}", "ID", "Title", "Author", "Status");
}
void print_row(const Book& book) {
std::string status = book.available() ? "on shelf" : "with " + *book.borrower;
std::println("{:<5}{:<16}{:<22}{}", book.id, book.title, book.author, status);
}Book is a plain struct because it has no rules of its own to protect; the Library class will guard the rules. to_lower converts text to lower case so that a search for "kalam" finds "Kalam". The cast to unsigned char before std::tolower is required: passing a negative char value, which can happen with non-English text, is undefined behaviour. The two print functions use the same column widths, so the header and rows always line up.
Step 3: the Library class
The class owns the map of books and hands out IDs starting from 101. Add it below the helpers.
class Library {
public:
int add_book(const std::string& title, const std::string& author) {
int id = next_id_++;
books_[id] = Book{id, title, author, std::nullopt};
return id;
}
void print_all() const {
print_header();
for (const Book& book : books_ | std::views::values) {
print_row(book);
}
}
std::size_t total() const { return books_.size(); }
std::size_t on_loan() const {
auto count = std::ranges::count_if(books_ | std::views::values,
[](const Book& b) { return !b.available(); });
return static_cast<std::size_t>(count);
}
// Steps 4 to 6 add more member functions here.
private:
std::map<int, Book> books_;
int next_id_ = 101;
};std::views::values walks over the Book part of each map entry, skipping the keys. std::ranges::count_if with a lambda counts the books that are out. The data members are private, so the only way to change a book is through the class's member functions.
Test the class with a temporary main at the end of the file:
int main() {
Library library;
library.add_book("Wings of Fire", "A. P. J. Abdul Kalam");
library.add_book("Godan", "Premchand");
library.add_book("The Guide", "R. K. Narayan");
library.add_book("Malgudi Days", "R. K. Narayan");
library.print_all();
std::println("Books: {}, on loan: {}", library.total(), library.on_loan());
}ID Title Author Status 101 Wings of Fire A. P. J. Abdul Kalam on shelf 102 Godan Premchand on shelf 103 The Guide R. K. Narayan on shelf 104 Malgudi Days R. K. Narayan on shelf Books: 4, on loan: 0
Step 4: lending and returning with std::expected
Lending can fail in two ways: the ID may not exist, or the book may already be out. Returning can fail if the ID is wrong or the book was never lent. Each function returns std::expected<void, std::string>: return {}; means success, and std::unexpected(message) carries the reason for failure. Add these inside the class, where the comment says.
std::expected<void, std::string> borrow(int id, const std::string& member) {
auto it = books_.find(id);
if (it == books_.end()) {
return std::unexpected(std::format("no book with id {}", id));
}
Book& book = it->second;
if (!book.available()) {
return std::unexpected(std::format("'{}' is already with {}", book.title, *book.borrower));
}
book.borrower = member;
return {};
}
std::expected<void, std::string> return_book(int id) {
auto it = books_.find(id);
if (it == books_.end()) {
return std::unexpected(std::format("no book with id {}", id));
}
if (it->second.available()) {
return std::unexpected(std::format("'{}' was not borrowed", it->second.title));
}
it->second.borrower.reset();
return {};
}std::format builds the error messages, and borrower.reset() empties the optional again. Test the new functions with a helper that prints either "OK" or the error:
void report(const std::expected<void, std::string>& result) {
if (result) {
std::println("OK");
} else {
std::println("Error: {}", result.error());
}
}
int main() {
Library library;
library.add_book("Wings of Fire", "A. P. J. Abdul Kalam");
library.add_book("Godan", "Premchand");
library.add_book("The Guide", "R. K. Narayan");
library.add_book("Malgudi Days", "R. K. Narayan");
report(library.borrow(102, "Priya"));
report(library.borrow(102, "Kabir"));
report(library.borrow(999, "Kabir"));
report(library.return_book(101));
report(library.return_book(102));
report(library.borrow(102, "Kabir"));
library.print_all();
}OK Error: 'Godan' is already with Priya Error: no book with id 999 Error: 'Wings of Fire' was not borrowed OK OK ID Title Author Status 101 Wings of Fire A. P. J. Abdul Kalam on shelf 102 Godan Premchand with Kabir 103 The Guide R. K. Narayan on shelf 104 Malgudi Days R. K. Narayan on shelf
Trace it: Priya borrows Godan; Kabir is refused because Priya has it; ID 999 does not exist; Wings of Fire was never lent; Priya returns Godan; now Kabir can borrow it. Every failure is reported clearly and none of them crashes the program.
Step 5: searching with ranges
The search should match any part of a title or author, ignoring case. A view pipeline filters the books, and std::ranges::to collects the matches into a vector. Add this member function to the class:
std::vector<Book> search(std::string_view words) const {
std::string needle = to_lower(words);
auto matches = books_
| std::views::values
| std::views::filter([&needle](const Book& b) {
return to_lower(b.title).contains(needle) ||
to_lower(b.author).contains(needle);
});
return std::ranges::to<std::vector>(matches);
}The function returns copies of the matching books rather than a view, because a view of books_ would become invalid as soon as the library changes. contains is the C++23 member function of std::string you met in Strings, string_view & format. With the four books from Step 3, library.search("narayan") returns books 103 and 104, and library.search("GUIDE") returns book 103.
Step 6: saving and loading
Each book becomes one line of text with fields separated by a vertical bar, for example 102|Godan|Premchand|Kabir. An empty last field means the book is on the shelf. Add these two member functions:
bool save(const std::string& path) const {
std::ofstream out(path);
if (!out) {
return false;
}
for (const Book& book : books_ | std::views::values) {
std::println(out, "{}|{}|{}|{}", book.id, book.title, book.author,
book.borrower.value_or(""));
}
return true;
}
bool load(const std::string& path) {
std::ifstream in(path);
if (!in) {
return false;
}
books_.clear();
std::string line;
while (std::getline(in, line)) {
std::istringstream fields(line);
std::string id_text, title, author, borrower;
std::getline(fields, id_text, '|');
std::getline(fields, title, '|');
std::getline(fields, author, '|');
std::getline(fields, borrower, '|');
int id = std::stoi(id_text);
Book book{id, title, author, std::nullopt};
if (!borrower.empty()) {
book.borrower = borrower;
}
books_[id] = book;
next_id_ = std::max(next_id_, id + 1);
}
return true;
}value_or("") writes an empty field for available books. When loading, next_id_ is moved past the largest ID in the file, so new books never reuse an old number. If the library from Step 4 is saved, library.txt contains:
101|Wings of Fire|A. P. J. Abdul Kalam|
102|Godan|Premchand|Kabir
103|The Guide|R. K. Narayan|
104|Malgudi Days|R. K. Narayan|This simple format breaks if a title itself contains a | character. A production program would reject such titles or use a proper format such as JSON or a SQLite database.
Step 7: the command loop
Finally, replace the test main with the real one. A helper reads a number safely, using the validation loop from User Input. The main loop reads a command word and calls the matching member function.
int read_int(std::string_view prompt) {
while (true) {
std::print("{}", prompt);
int value = 0;
if (std::cin >> value) {
return value;
}
if (std::cin.eof()) {
return -1;
}
std::println("Please type a number.");
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
}
int main() {
const std::string data_file = "library.txt";
Library library;
if (library.load(data_file)) {
std::println("Loaded {} books from {}", library.total(), data_file);
} else {
std::println("No saved data found, starting a new library.");
}
std::println("Commands: list, add, borrow, return, search, stats, quit");
std::string command;
while (true) {
std::print("> ");
if (!(std::cin >> command) || command == "quit") {
break;
}
if (command == "list") {
library.print_all();
} else if (command == "add") {
std::string title, author;
std::print("Title: ");
std::getline(std::cin >> std::ws, title);
std::print("Author: ");
std::getline(std::cin >> std::ws, author);
std::println("Added book {}", library.add_book(title, author));
} else if (command == "borrow") {
int id = read_int("Book id: ");
std::string member;
std::print("Member name: ");
std::getline(std::cin >> std::ws, member);
if (auto result = library.borrow(id, member)) {
std::println("Issued to {}. Please return it within 14 days.", member);
} else {
std::println("Sorry: {}", result.error());
}
} else if (command == "return") {
int id = read_int("Book id: ");
if (auto result = library.return_book(id)) {
std::println("Returned. Thank you!");
} else {
std::println("Sorry: {}", result.error());
}
} else if (command == "search") {
std::string words;
std::print("Search for: ");
std::getline(std::cin >> std::ws, words);
auto found = library.search(words);
if (found.empty()) {
std::println("No matches.");
}
for (const Book& book : found) {
print_row(book);
}
} else if (command == "stats") {
std::println("{} books, {} on loan", library.total(), library.on_loan());
} else {
std::println("Unknown command: {}", command);
}
}
if (library.save(data_file)) {
std::println("Saved {} books to {}. Goodbye!", library.total(), data_file);
} else {
std::println("Could not save to {}", data_file);
}
}Here is a sample session. It starts with the library.txt shown in Step 6, and the text after each prompt is what the librarian typed:
Loaded 4 books from library.txt Commands: list, add, borrow, return, search, stats, quit > add Title: Gitanjali Author: Rabindranath Tagore Added book 105 > borrow Book id: 102 Member name: Priya Sharma Sorry: 'Godan' is already with Kabir > return Book id: 102 Returned. Thank you! > borrow Book id: 102 Member name: Priya Sharma Issued to Priya Sharma. Please return it within 14 days. > search Search for: tagore 105 Gitanjali Rabindranath Tagore on shelf > stats 5 books, 1 on loan > quit Saved 5 books to library.txt. Goodbye!
Notice how the pieces fit: std::ws skips the newline left by >> before each getline, so titles and names with spaces work; failures from borrow and return_book arrive as readable messages; and the data is saved on every normal exit, including when the input ends.
What you used, and where to go next
| Feature | Where it appears |
|---|---|
struct, class, const member functions | Book, Library |
std::map, std::optional | Book storage and the borrower field |
std::expected, std::unexpected | borrow and return_book |
Views, count_if, std::ranges::to | print_all, on_loan, search |
std::format, std::println | Error messages and aligned tables |
std::ofstream, std::ifstream, std::istringstream | save and load |
Input validation with std::cin | read_int and the command loop |
The program is small, but its structure scales: the Library class knows nothing about the console, so you could put a graphical or web interface on top of it without changing it. Good next steps are splitting the code into library.h, library.cpp and main.cpp (see Classes & Objects), adding due dates with <chrono>, and writing automated tests for borrow and return_book.
Key points
- Plan features and data before coding; choose types that express meaning, such as
std::optionalfor "maybe a borrower". - Keep rules inside a class with private data, and report expected failures with
std::expectedinstead of crashing. - Ranges and views keep searching and counting short and readable; return containers, not views, from member functions.
- Save data in a simple text format with file streams, and update the next ID after loading.
- Separate the user interface (the command loop) from the logic (the
Libraryclass).
Exercise
Extend the library manager with a remove command that deletes a book by ID. It must refuse to remove a book that is currently on loan. Add a member function std::expected<void, std::string> remove_book(int id) to Library and a matching branch in the command loop, then test it by removing an available book and a borrowed one.
Show solution
// inside class Library
std::expected<void, std::string> remove_book(int id) {
auto it = books_.find(id);
if (it == books_.end()) {
return std::unexpected(std::format("no book with id {}", id));
}
if (!it->second.available()) {
return std::unexpected(std::format("'{}' is on loan to {}", it->second.title,
*it->second.borrower));
}
books_.erase(it);
return {};
}// inside the command loop in main
} else if (command == "remove") {
int id = read_int("Book id: ");
if (auto result = library.remove_book(id)) {
std::println("Book {} removed.", id);
} else {
std::println("Sorry: {}", result.error());
}> remove Book id: 103 Book 103 removed. > remove Book id: 102 Sorry: 'Godan' is on loan to Priya Sharma
remove_book follows the same pattern as borrow: look up the ID, check the rule, and only then change the map. Also add remove to the list of commands printed at start-up.
Quick Quiz
1. Why does Book store the borrower as std::optional<std::string>?
std::optional makes "no borrower" a clear, checked state instead of a special string.
2. What does borrow return when the book is already on loan?
Expected failures are returned as std::unexpected(message), so the caller can print them.
3. Why does search return a std::vector<Book> instead of a view?
Returning copies keeps the result valid no matter what happens to the library later.
4. Why is next_id_ updated while loading?
After loading, the next ID must be larger than every saved ID.
Learn C++ with a mentor
Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
