Contents

Chapter 21

Interlude: Build Something Real

No new syntax. An expense tracker that remembers.

There is nothing new in this chapter. Not one keyword, not one header.

That’s the point of it. You have spent twenty chapters collecting parts, and the gap between knowing what a std::map is and reaching for one unprompted is the gap between reading about programming and doing it. This chapter closes it by having you build something with no new material to hide behind.

We’re building an expense tracker. It records what you spent, on what, and in what category; it totals things up; it remembers between runs; and it doesn’t fall over when you type nonsense at it.

Total: 915.75

By category
  food      15.75
  housing   900.00

Biggest
  rent 900.00
  lunch 12.25
  coffee 3.50

Read the whole chapter before writing anything. Then build it in the five stages below, running it at the end of every stage. A program that works after each step is a program where the bug you just introduced is in the twenty lines you just wrote.

Stage 1: the type

class Expense {
public:
    Expense(std::string d, std::string c, double a);

    const std::string& description() const;
    const std::string& category() const;
    double amount() const;

private:
    std::string desc;
    std::string cat;
    double amt;
};

Chapters 18 and 19, unchanged. Fields private, three const accessors returning const& for the strings and a plain double for the number, chapter 12’s rule, applied without thinking about it by now.

The invariant is that an amount is never negative, and the constructor enforces it:

Expense::Expense(std::string d, std::string c, double a)
    : desc(d), cat(c), amt(a) {
    if (amt < 0) {
        amt = 0;
    }
}

Note the Expense:: in front. Defining a member function outside the class body needs it, because it says which class this belongs to. You’ll write it a lot in stage 5, and it’s the same name-then-body split as any other function.

Stop and run it. A main that makes three expenses and prints them is enough.

Stage 2: the list, and the arithmetic

A std::vector<Expense> and three questions to ask it.

The total is chapter 10’s accumulate-by-hand or chapter 13’s std::accumulate though accumulate over a vector of Expense needs more than you’ve been shown, so a range-for and a running double total is the right call here. Notice that you reached for the fancier tool and it didn’t fit; that happens constantly and is not a failure.

The total per category is chapter 16, and it is three lines:

std::map<std::string, double> by_cat;
for (const Expense& e : expenses) {
    by_cat[e.category()] += e.amount();
}

That += on a key that might not exist works for exactly the reason chapter 16’s counting idiom did: [] creates it with a zero first. Same behaviour, different job.

The biggest first is chapter 13 and 14 together:

bool by_amount(const Expense& a, const Expense& b) {
    return a.amount() > b.amount();
}

std::sort(expenses.begin(), expenses.end(), by_amount);

For money, print with two decimal places. Chapter 6’s <iomanip>, once, before you print anything:

std::cout << std::fixed << std::setprecision(2);

Stop and run it. Hardcode four expenses and check the numbers by hand. This is the last stage where you can.

Stage 3: remembering

Chapter 15, with chapter 15’s warning about when to open the file for writing.

One expense per line, fields separated by |:

std::string Expense::to_line() const {
    return desc + "|" + cat + "|" + std::to_string(amt);
}
coffee|food|3.500000
rent|housing|900.000000

std::to_string on a double gives six decimal places whether you want them or not. It’s ugly and it round-trips correctly, so leave it. A save file is for the program, not for you. If it bothers you, that’s a stretch at the end.

Reading back needs std::istringstream from chapter 15, and this is where chapter 17 earns its place:

std::optional<Expense> parse(const std::string& line) {
    std::istringstream parts(line);
    std::string d, c, a;

    if (!std::getline(parts, d, '|')) { return std::nullopt; }
    if (!std::getline(parts, c, '|')) { return std::nullopt; }
    if (!std::getline(parts, a))      { return std::nullopt; }

    return Expense(d, c, std::stod(a));
}

A line that doesn’t have three fields isn’t an expense, and parse says so honestly instead of returning something half-built. The loader skips what it can’t read:

skipping bad line: [this line is broken]
coffee / food / 3.5
rent / housing / 900

Stop and run it twice. Add expenses, quit, start again, and check they’re still there. Then open the file in your editor, type rubbish into the middle of it, and confirm the program survives.

Stage 4: the menu, and not falling over

Chapter 17’s read_int, doing the job it was built for:

1) Add  2) List  3) By category  4) Biggest  5) Quit
Choice:

Everything the reader types is outside chapter 17’s line:

Stop and try to break it. Everything from chapter 17’s project: letters at the menu, an empty line, a negative amount, an enormous number, a category with a | in it. That last one will corrupt your save file, and deciding what to do about it is a genuine design question rather than a bug to fix.

Stage 5: three files

Chapter 20, on a program that has earned it.

FileHolds
expense.hthe Expense class, and parse
expense.cppthe constructor, the accessors, to_line, parse
main.cppload, save, the reports, the menu loop

Move one thing at a time and build after each. Add the Makefile last.

CXXFLAGS = -std=c++17 -Wall -Wextra -g

tracker: main.o expense.o
	g++ $(CXXFLAGS) main.o expense.o -o tracker

main.o: main.cpp expense.h
	g++ $(CXXFLAGS) -c main.cpp

expense.o: expense.cpp expense.h
	g++ $(CXXFLAGS) -c expense.cpp

clean:
	rm -f tracker main.o expense.o

Remember the tab.

Part 1 is finished when

That last one is the real test. The syntax is checkable by the compiler; knowing which container answers which question is the part that’s yours.

What you can do now

Twenty-one chapters ago you had no compiler. You can now write a program that holds structured data, keeps it correct by construction, sorts and summarises it, survives a hostile reader, remembers itself between runs, and is split across files with a build that only rebuilds what changed.

That is a genuinely useful amount of C++. Plenty of working programmers write mostly this.

What you can’t do yet

Here’s the honest part.

You don’t know what a variable is. You’ve never seen an address. You don’t know what std::string is made of, or why std::vector had to be invented, or what the <int> in the angle brackets actually does, and chapter 10 promised that one to chapter 28 and it’s still owed. You don’t know what happens to Expense when it stops existing, or why returning a big vector from a function turned out not to be slow.

And when you read other people’s C++, whether older code, library code or anything close to the hardware, you will hit * and & and new and delete doing things you can’t parse.

Part 2 is about the floor under everything you just built. It goes back to memory, addresses and pointers, and rebuilds your picture of what has been happening this whole time. Nothing in Part 1 turns out to be a lie; a lot of it turns out to be a summary.

Finish the tracker first. Then chapter 22 asks a question with a longer answer than it deserves: what is a variable, really?

Project

The expense tracker

Roughly a few evenings

Everything above, built in the five stages, in that order, running at the end of each.

Do not start with the file at stage 3 or the split at stage 5. Every stage is runnable on purpose, and the discipline of keeping it that way is most of what makes a program this size finishable.

Stretch one, plug the stod hole. Make parse refuse a third field that isn’t a number, so a hand-edited file can’t take the program down. An std::istringstream and a checked read is the tidiest way: read a double out of the field and see whether the stream is still happy, which is chapter 17’s whole lesson applied one level down.

Stretch two, dates and a real report. Add a date to Expense, as a string in YYYY-MM-DD form so that sorting it alphabetically also sorts it chronologically. Then report a monthly total, keyed on the first seven characters, which chapter 4’s substr gives you.

Stretch three, budgets. A second file mapping category to a monthly limit, and a report showing what’s over. This needs a second std::map, a second save file, and a decision about what happens to a category with no budget set: count before [], and chapter 16’s trap avoided on purpose.

Stretch four, the one worth doing. Give it to somebody and watch them use it without helping. Write down every moment they hesitate. That list is a better guide to what to build next than anything you would have thought of alone.