Project: a library
No new features this lesson. Instead, we'll put what you know together into a small program: the lending records of a neighborhood library. There are a few books on the shelf; people borrow them and bring them back; and at any time you can see who has what. We'll build it step by step, and every step is a complete program you can run.
The plan
First, work out what to keep track of and what to do:
| To keep track of | Use |
|---|---|
| one book: title, author, whether it's out | class Book |
| every book on the shelf | std::vector<Book> |
| who has which books | std::map<std::string, std::vector<std::string>>: a name maps to a list of titles |
Each thing to do becomes a function: add a book, find a book by title, show the shelf, borrow, return, show who has what.
Step 1: the Book class
A book has three things: a title, an author, and whether it's out. The members are private and read through member functions; only "whether it's out" can be changed:
1#include <iostream>2#include <string>3 4class Book {5public:6 Book(const std::string& title, const std::string& author)7 : title_(title), author_(author), borrowed_(false) {}8 const std::string& title() const { return title_; }9 const std::string& author() const { return author_; }10 bool borrowed() const { return borrowed_; }11 void set_borrowed(bool value) { borrowed_ = value; }12 13private:14 std::string title_;15 std::string author_;16 bool borrowed_;17};18 19void show_book(const Book& b) {20 std::cout << '"' << b.title() << "\" by " << b.author();21 if (b.borrowed()) {22 std::cout << " (out)";23 }24 std::cout << '\n';25}26 27int main() {28 Book b{"Dune", "Frank Herbert"};29 show_book(b);30 b.set_borrowed(true);31 show_book(b);32 return 0;33}
"Dune" by Frank Herbert "Dune" by Frank Herbert (out)
The member functions that read the title and author return const std::string&: no copy, and nobody outside can change it. show_book is an ordinary function that only reads, so its parameter is const Book&.
Step 2: the shelf
The shelf is a std::vector<Book>. Add three functions: add_book puts a book on the shelf, find_book looks one up by title and returns its index (or -1 if it isn't there), and show_books shows them one by one:
1#include <iostream>2#include <string>3#include <vector>4 5class Book {6public:7 Book(const std::string& title, const std::string& author)8 : title_(title), author_(author), borrowed_(false) {}9 const std::string& title() const { return title_; }10 const std::string& author() const { return author_; }11 bool borrowed() const { return borrowed_; }12 void set_borrowed(bool value) { borrowed_ = value; }13 14private:15 std::string title_;16 std::string author_;17 bool borrowed_;18};19 20void show_book(const Book& b) {21 std::cout << '"' << b.title() << "\" by " << b.author();22 if (b.borrowed()) {23 std::cout << " (out)";24 }25 std::cout << '\n';26}27 28void add_book(std::vector<Book>& books, const std::string& title, const std::string& author) {29 books.push_back(Book{title, author});30}31 32int find_book(const std::vector<Book>& books, const std::string& title) {33 for (std::size_t i = 0; i < books.size(); i++) {34 if (books[i].title() == title) {35 return static_cast<int>(i);36 }37 }38 return -1;39}40 41void show_books(const std::vector<Book>& books) {42 for (const auto& b : books) {43 show_book(b);44 }45}46 47int main() {48 std::vector<Book> books;49 add_book(books, "Dune", "Frank Herbert");50 add_book(books, "Emma", "Jane Austen");51 add_book(books, "Beloved", "Toni Morrison");52 show_books(books);53 std::cout << "Emma: index " << find_book(books, "Emma") << '\n';54 std::cout << "Ulysses: index " << find_book(books, "Ulysses") << '\n';55 return 0;56}
"Dune" by Frank Herbert "Emma" by Jane Austen "Beloved" by Toni Morrison Emma: index 1 Ulysses: index -1
Why does find_book return an index and not a Book& or a pointer? As lesson 28 showed, adding more books can make the vector move, and then references and pointers from before are no good. An index is always fine.
Step 3: who has what
The lending record is a map: the key is a person's name, and the value is the list of titles they have (a vector). Try it on its own first to see its shape. Press Step and watch each name in loans get its own row of boxes:
1#include <iostream>2#include <map>3#include <string>4#include <vector>5 6void show_loans(const std::map<std::string, std::vector<std::string>>& loans) {7 if (loans.empty()) {8 std::cout << "Nobody has a book\n";9 }10 for (const auto& kv : loans) {11 std::cout << kv.first << ":";12 for (const auto& t : kv.second) {13 std::cout << " \"" << t << '"';14 }15 std::cout << '\n';16 }17}18 19int main() {20 std::map<std::string, std::vector<std::string>> loans;21 show_loans(loans);22 loans["Ana"].push_back("Dune");23 loans["Ben"].push_back("Emma");24 loans["Ana"].push_back("Beloved");25 show_loans(loans);26 return 0;27}
Nobody has a book Ana: "Dune" "Beloved" Ben: "Emma"
loans["Ana"].push_back(...) relies on what lesson 13 said: square brackets create the key if it's missing. The first time Ana borrows, the map makes her an empty list, then the title goes in. Here that's exactly what we want; but when you only want to look, it's a trap:
What does the last line show?
1#include <iostream>2#include <map>3#include <string>4#include <vector>5 6int main() {7 std::map<std::string, std::vector<std::string>> loans;8 loans["Ana"].push_back("Dune");9 std::cout << "Ben has " << loans["Ben"].size() << " books\n";10 std::cout << loans.size() << '\n';11 return 0;12}
loans["Ben"] doesn't find Ben, so it makes him an empty list. Now there are two people in the map, Ana and Ben, and show_loans would show an extra line, "Ben:". That's why returning a book, below, looks people up with find.
Step 4: borrowing
Borrowing checks two things first: does the book exist, and is it already out? If both are fine, mark the book as out and add it to the person's list:
1#include <iostream>2#include <map>3#include <string>4#include <vector>5 6class Book {7public:8 Book(const std::string& title, const std::string& author)9 : title_(title), author_(author), borrowed_(false) {}10 const std::string& title() const { return title_; }11 const std::string& author() const { return author_; }12 bool borrowed() const { return borrowed_; }13 void set_borrowed(bool value) { borrowed_ = value; }14 15private:16 std::string title_;17 std::string author_;18 bool borrowed_;19};20 21int find_book(const std::vector<Book>& books, const std::string& title) {22 for (std::size_t i = 0; i < books.size(); i++) {23 if (books[i].title() == title) {24 return static_cast<int>(i);25 }26 }27 return -1;28}29 30void borrow_book(std::vector<Book>& books, std::map<std::string, std::vector<std::string>>& loans,31 const std::string& person, const std::string& title) {32 int i = find_book(books, title);33 if (i == -1) {34 std::cout << "There is no \"" << title << "\"\n";35 return;36 }37 if (books[i].borrowed()) {38 std::cout << '"' << title << "\" is already out\n";39 return;40 }41 books[i].set_borrowed(true);42 loans[person].push_back(title);43 std::cout << person << " took \"" << title << "\"\n";44}45 46int main() {47 std::vector<Book> books;48 books.push_back(Book{"Dune", "Frank Herbert"});49 books.push_back(Book{"Emma", "Jane Austen"});50 std::map<std::string, std::vector<std::string>> loans;51 borrow_book(books, loans, "Ana", "Dune");52 borrow_book(books, loans, "Ben", "Dune");53 borrow_book(books, loans, "Ben", "Ulysses");54 borrow_book(books, loans, "Ben", "Emma");55 std::cout << "Ben has " << loans["Ben"].size() << " book\n";56 return 0;57}
Ana took "Dune" "Dune" is already out There is no "Ulysses" Ben took "Emma" Ben has 1 book
books and loans both get changed, so they're passed by reference; person and title are only read, so they're passed by const reference. When something's wrong, the function shows a line and returns, skipping the rest: rule out the bad cases first, and what's left is an ordinary loan.
Step 5: returning
Returning works the other way: first use find to see whether the person has any loans (not square brackets, which would create one), then find the title in their list and erase it as in lesson 27. If the list is now empty, remove the person from the map altogether. Here is the whole program, playing out a day at the library:
1#include <algorithm>2#include <iostream>3#include <map>4#include <string>5#include <vector>6 7class Book {8public:9 Book(const std::string& title, const std::string& author)10 : title_(title), author_(author), borrowed_(false) {}11 const std::string& title() const { return title_; }12 const std::string& author() const { return author_; }13 bool borrowed() const { return borrowed_; }14 void set_borrowed(bool value) { borrowed_ = value; }15 16private:17 std::string title_;18 std::string author_;19 bool borrowed_;20};21 22int find_book(const std::vector<Book>& books, const std::string& title) {23 for (std::size_t i = 0; i < books.size(); i++) {24 if (books[i].title() == title) {25 return static_cast<int>(i);26 }27 }28 return -1;29}30 31void borrow_book(std::vector<Book>& books, std::map<std::string, std::vector<std::string>>& loans,32 const std::string& person, const std::string& title) {33 int i = find_book(books, title);34 if (i == -1 || books[i].borrowed()) {35 std::cout << "Can't lend \"" << title << "\"\n";36 return;37 }38 books[i].set_borrowed(true);39 loans[person].push_back(title);40}41 42void return_book(std::vector<Book>& books, std::map<std::string, std::vector<std::string>>& loans,43 const std::string& person, const std::string& title) {44 auto found = loans.find(person);45 if (found == loans.end()) {46 std::cout << person << " has no books\n";47 return;48 }49 std::vector<std::string>& titles = found->second;50 auto it = std::find(titles.begin(), titles.end(), title);51 if (it == titles.end()) {52 std::cout << person << " doesn't have \"" << title << "\"\n";53 return;54 }55 titles.erase(it);56 if (titles.empty()) {57 loans.erase(found);58 }59 books[find_book(books, title)].set_borrowed(false);60 std::cout << person << " returned \"" << title << "\"\n";61}62 63void show_loans(const std::map<std::string, std::vector<std::string>>& loans) {64 for (const auto& kv : loans) {65 std::cout << kv.first << ":";66 for (const auto& t : kv.second) {67 std::cout << " \"" << t << '"';68 }69 std::cout << '\n';70 }71}72 73int main() {74 std::vector<Book> books;75 books.push_back(Book{"Dune", "Frank Herbert"});76 books.push_back(Book{"Emma", "Jane Austen"});77 books.push_back(Book{"Beloved", "Toni Morrison"});78 std::map<std::string, std::vector<std::string>> loans;79 borrow_book(books, loans, "Ana", "Dune");80 borrow_book(books, loans, "Ana", "Beloved");81 borrow_book(books, loans, "Ben", "Emma");82 show_loans(loans);83 return_book(books, loans, "Ben", "Emma");84 return_book(books, loans, "Ana", "Emma");85 return_book(books, loans, "Cara", "Dune");86 show_loans(loans);87 return 0;88}
Ana: "Dune" "Beloved" Ben: "Emma" Ben returned "Emma" Ana doesn't have "Emma" Cara has no books Ana: "Dune" "Beloved"
Press Step and watch Ben's whole row vanish from loans once he returns his book. After loans.erase(found), the list titles was tied to is gone too, so titles mustn't be used after that; and here it isn't.
Looking back
About a hundred lines, and they use a lot of this course:
- a
classhides a book's data and keeps only the member functions it needs (lesson 15); - a
vectoris the shelf, and amapofvectors holds the loans, each doing its own job; - functions take what they change by reference and what they only read by
constreference (lesson 10); - finding a book returns an index, not a reference (lesson 28);
- when you only want to look, use
find, not square brackets (lesson 13); - an iterator and
eraseremove one item from a list (lesson 27).
Tip: As the program grows, you could put books and loans together in a class Library, with borrowing and returning as its member functions; then main wouldn't have to pass two containers every time.
Practice
Write the code, then press Check. A hint costs nothing; the answer is there when you need it. Each one adds a feature to the library.
How many on the shelf
✓Finish count_available: count the books that aren't out. main is already written.
3 books on the shelf
1#include <iostream>2#include <string>3#include <vector>4 5class Book {6public:7 Book(const std::string& title, bool borrowed) : title_(title), borrowed_(borrowed) {}8 const std::string& title() const { return title_; }9 bool borrowed() const { return borrowed_; }10 11private:12 std::string title_;13 bool borrowed_;14};15 16int count_available(const std::vector<Book>& books) {17 int count = 0;18 19 return count;20}21 22int main() {23 std::vector<Book> books;24 books.push_back(Book{"Dune", true});25 books.push_back(Book{"Emma", false});26 books.push_back(Book{"Beloved", false});27 books.push_back(Book{"Hamlet", true});28 books.push_back(Book{"Persuasion", false});29 std::cout << count_available(books) << " books on the shelf\n";30 return 0;31}
Books by author
✓Finish show_by_author: show each book by that author, one per line; if there are none, show No books by that author.
"Emma" "Persuasion" No books by that author
1#include <iostream>2#include <string>3#include <vector>4 5class Book {6public:7 Book(const std::string& title, const std::string& author) : title_(title), author_(author) {}8 const std::string& title() const { return title_; }9 const std::string& author() const { return author_; }10 11private:12 std::string title_;13 std::string author_;14};15 16void show_by_author(const std::vector<Book>& books, const std::string& author) {17 18}19 20int main() {21 std::vector<Book> books;22 books.push_back(Book{"Emma", "Jane Austen"});23 books.push_back(Book{"Dune", "Frank Herbert"});24 books.push_back(Book{"Persuasion", "Jane Austen"});25 show_by_author(books, "Jane Austen");26 show_by_author(books, "Leo Tolstoy");27 return 0;28}
Two books each, at most
✓Give borrow_book a rule: if the person already has 2 books, show Ana already has 2 books and don't lend them another.
Ana took "Dune" Ana took "Emma" Ana already has 2 books Ben took "Beloved"
1#include <iostream>2#include <map>3#include <string>4#include <vector>5 6void borrow_book(std::map<std::string, std::vector<std::string>>& loans,7 const std::string& person, const std::string& title) {8 loans[person].push_back(title);9 std::cout << person << " took \"" << title << "\"\n";10}11 12int main() {13 std::map<std::string, std::vector<std::string>> loans;14 borrow_book(loans, "Ana", "Dune");15 borrow_book(loans, "Ana", "Emma");16 borrow_book(loans, "Ana", "Beloved");17 borrow_book(loans, "Ben", "Beloved");18 return 0;19}
What you learned
- First work out what to keep track of (which containers) and what to do (which functions), then build it step by step, running each step.
- One thing's data goes in a
class, withprivatemembers read and changed through member functions. - A group of things goes in a
vector; "who goes with what" goes in amap, whose values can bevectors too. - Containers you change are passed by reference; ones you only read, by
constreference. - Keep an index, not a reference, to a
vectorelement; look things up without creating them withfind; remove one item from a list with an iterator anderase. - Rule out the bad cases at the top of a function (show a line,
return); what's left is the normal case.