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:
- the menu choice: must be a number, must be 1 to 5
- the amount: must be a number, and the constructor clamps negatives, but a friendly refusal beats a silent clamp
- the description and category:
std::getline, because they contain spaces, with chapter 5’signore()after any>> - an empty category should become something like
uncategorisedrather than an empty map key
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.
| File | Holds |
|---|---|
expense.h | the Expense class, and parse |
expense.cpp | the constructor, the accessors, to_line, parse |
main.cpp | load, 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.