Kodewick
Lesson 30 of 30Contents

Project: a library

Lesson 30 of 30 · Part 7 · Going further · about 20 minutes

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 ofUse
one book: title, author, whether it's outclass Book
every book on the shelfstd::vector<Book>
who has which booksstd::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:

main.cpp
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}
Output
"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:

main.cpp
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}
Output
"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:

main.cpp
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}
Output
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:

Guess first

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}

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:

main.cpp
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}
Output
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:

main.cpp
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}
Output
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 class hides a book's data and keeps only the member functions it needs (lesson 15);
  • a vector is the shelf, and a map of vectors holds the loans, each doing its own job;
  • functions take what they change by reference and what they only read by const reference (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 erase remove 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.

Exercise 1

How many on the shelf

Finish count_available: count the books that aren't out. main is already written.

It should print
3 books on the shelf
exercise1.cpp
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}
Output
Press Run or Ctrl+Enter
Exercise 2

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.

It should print
"Emma"
"Persuasion"
No books by that author
exercise2.cpp
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}
Output
Press Run or Ctrl+Enter
Exercise 3

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.

It should print
Ana took "Dune"
Ana took "Emma"
Ana already has 2 books
Ben took "Beloved"
exercise3.cpp
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}
Output
Press Run or Ctrl+Enter

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, with private members read and changed through member functions.
  • A group of things goes in a vector; "who goes with what" goes in a map, whose values can be vectors too.
  • Containers you change are passed by reference; ones you only read, by const reference.
  • Keep an index, not a reference, to a vector element; look things up without creating them with find; remove one item from a list with an iterator and erase.
  • Rule out the bad cases at the top of a function (show a line, return); what's left is the normal case.