Coin-Tray Auditor
You've inherited a tiny maintenance tool for a vending machine. The Coin-Tray Auditor is a little library of functions that totals the cash inside a machine: the value of a pile of coins, the value of a loose coin tray, the change a customer is owed, the value of full bank rolls of quarters, and the grand-total audit number printed on the maintenance ticket.
There's a catch. The code compiles cleanly and runs — and still gives wrong answers. That is the whole lesson of Chapter 3: a clean compile only rules out syntax errors and compile-time semantic errors; it says nothing about logical errors, where valid C++ computes the wrong thing (notes §3.1). Your job is not to write this program — it's to debug it. You'll work a real bug report, compare each function's expected output against what it actually produces, and find the first place they diverge (notes §3.3). There are six planted bugs, one of each classic beginner flavor.
Your tasks
coinWorth— wrong operator. The value ofcountcoins is a product, not a sum. MakecoinWorth(3, kQuarter)give75.trayValue— copy-paste slip. One line was duplicated from its neighbour and never updated, so it multiplies by the wrong coin constant. Match every term's constant to the variable it scales.changeOwed— swapped operands. Subtraction isn't commutative; the change is coming out negative. Order the operands so change is non-negative.rollsValue— off-by-one constant. A roll is 40 quarters, not the hard-coded number. UsekQuartersPerRollfrom the header so it can't drift.machineCents— two bugs: swapped call arguments + wrong initial value. The call totrayValuepasses its arguments in the wrong order, and the running total starts from a stray seed instead of0. Fix both.
Success criteria
make test prints PASS ✅ all checks — every planted bug is fixed. Until
then it prints, for each remaining bug, the failing expression and its line, e.g.
FAIL: coinWorth(3, kQuarter) == 75 @line 26 FAIL: ... FAIL ❌ N check(s) failing. Fix the FIRST one, rebuild, repeat.
Work it like the chapter teaches: read the first failure, fix that one
function, rebuild, repeat. Each failing line names the function to inspect;
its EXPECTED: comment in the header tells you what it should compute. Turning
that red into green — six bugs, one localized fix at a time — is the exercise.
Concepts practiced
- Syntax vs. compile-time-semantic vs. logical errors — and why the last kind sails right past the compiler (3.1).
- The debugging strategy: compare expected state to actual state and localize the first divergence (3.2–3.3).
- Functions as known-good / known-bad checkpoints — each function is a stage you can confirm in isolation, then trust (3.3, "trace from known-good to known-bad").
- The six classic logical bugs: wrong operator, copy-paste slip, swapped operands, off-by-one constant, swapped call arguments, wrong initial value.
- Reused from earlier chapters: user-defined functions, parameters & return values, the header/source split and header guards (Ch 2); variable initialization and arithmetic expressions (Ch 1).
Constraints
- Debug; don't rewrite. Make the smallest correct fix per bug (notes §3.2, "avoid debugging by demolition"). Each bug is a one-token / one-line change.
- Do not edit
coin_tray.hortests/tests.cpp— the contracts and the bug report are fixed. You only editstarter/coin_tray.cpp. - Stay in scope: straight-line arithmetic and function calls only. No
if, no loops, no new types — none are taught yet, and none are needed. - Keep every variable initialized and the code warning-clean
(
-Wall -Wextra). - You may delete a
// BUG?breadcrumb once you've confirmed that line is correct, but you don't have to.
Build & run locally
make # compile starter/coin_tray.cpp -> proves it BUILDS (clean compile != correct)
make test # grade your code against the bug report (RED until all six bugs are fixed)
make solution # run the grader against the reference solution if you're stuck
make clean # remove build artifactsmake run is an alias for make test (this is a library — running it is
grading it).
Hints
How to attack this (read first)
Don't scan for "wrong-looking" code. Let the evidence drive (notes §3.3).
Run make test, take the first FAIL line, open that function, and put its
// BUG? line next to the header's EXPECTED: promise. Ask: does this line
compute the promise? When two values disagree, you've found the divergence.
Fix only that, then rebuild — small change, clear cause and effect.
Task 1 — coinWorth
Three quarters is 3 * 25 = 75. The body adds. What operator turns "three of
something worth 25" into 75? (This is the area = width + height bug from
notes §3.1, in miniature.)
Task 2 — trayValue
Run the isolating checks in your head: trayValue(0,1,0,0) should be 10 (one
dime). Once Task 1 is fixed so coinWorth multiplies, this call comes out 5 —
and a dime valued at 5 means the dime line is using the nickel constant
kNickel. It should use kDime.
Task 3 — changeOwed
changeOwed(100, 75) printing -25 is the tell: the result is the exact
negative of the right answer. a - b vs b - a. Which order gives +25?
Task 4 — rollsValue
rollsValue(1) is one roll = 40 * 25 = 1000. Once Task 1 is fixed so
coinWorth multiplies, this call prints 975 = 39 * 25 — the literal 39 is
off by one. Replace it with kQuartersPerRoll (defined as 40 in the header) —
named constants don't drift.
Task 5 — machineCents (two bugs)
Two separate slips:
- Swapped arguments.
trayValue's parameters are(quarters, dimes, nickels, pennies). The call hands itdimesandquartersin the wrong slots — they get valued at each other's denomination. This is the notes §3.9 "caller passed the bad value" case: the symptom is intrayValue, the cause is the call. - Wrong initial value. The accumulator starts at
kQuarter(25), so the total is always 25c too high. Accumulators start at0(notes §3.8: a local that doesn't begin where you assumed).
Stretch goals
- Hard mode: before reading any
// BUG?flag, delete all of them, then re-find the six bugs using only the grader's failures and the header contracts. That's the real-world experience — bugs don't announce themselves. - Print-debugging drill (notes §3.4): add temporary
std::cerrtraces insidemachineCents— e.g.std::cerr << "[audit] loose=" << loose << '\n';— to watchlooseandrolledand confirm where the number goes wrong. Usestd::cerr, notstd::cout, and remove the traces before you're done. - Regression test: add one new
CHECKto a copy of the grader that would have caught the off-by-one inrollsValuefrom a different input (e.g.rollsValue(10) == 10000) — practice writing a boundary test (notes §3.10). - Add an
assert-guarded contract such as "change is never negative" once you reach assertions (Chapter 9).
// coin_tray.cpp — Vending-Machine Coin-Tray Auditor (STARTER / BUGGY)
//
// This program COMPILES and RUNS, warning-clean. That is exactly the danger of
// this chapter: a clean compile only rules out syntax errors and compile-time
// semantic errors. It says NOTHING about logical errors — code that builds and
// runs but computes the wrong answer (notes/chapter-03.md, §3.1).
//
// There are SIX planted logical bugs in the bodies below. Each is one of the
// classic beginner mistakes:
// wrong operator · copy-paste slip · swapped operands ·
// off-by-one constant · swapped call arguments · wrong initial value
//
// Every suspect line is flagged with a `// BUG?` breadcrumb. A breadcrumb is a
// HYPOTHESIS, not a confession: some are genuinely wrong, and you must confirm
// each one by comparing what the line computes against the EXPECTED contract in
// coin_tray.h. Fix the real bugs; if a flagged line is actually fine, you may
// delete its breadcrumb. `make test` is RED now and turns GREEN when all six
// are fixed.
//
// SCOPE: straight-line arithmetic and function calls only — no if, no loops
// (those are later chapters). Every bug here lives in the math, the operands,
// the constants, or the call arguments.
#include "../coin_tray.h"
// ─── TASK 1: fix coinWorth — value of `count` coins of `coinValue` cents ────
// EXPECTED (per coin_tray.h): count * coinValue. e.g. coinWorth(3, 25) == 75.
// Compare the operator below against that promise.
//
// >>> YOUR CODE HERE <<<
//
int coinWorth(int count, int coinValue)
{
return count + coinValue; // BUG?
}
// ────────────────────────────────────────────────────────────────────────────
// ─── TASK 2: fix trayValue — total cents of a loose coin tray ───────────────
// EXPECTED: quarters*25 + dimes*10 + nickels*5 + pennies*1.
// One line was copy-pasted from its neighbour and never updated. Read each
// term and check the coin constant matches the variable it multiplies.
//
// >>> YOUR CODE HERE <<<
//
int trayValue(int quarters, int dimes, int nickels, int pennies)
{
int q { coinWorth(quarters, kQuarter) };
int d { coinWorth(dimes, kNickel) }; // BUG?
int n { coinWorth(nickels, kNickel) };
int p { coinWorth(pennies, kPenny) };
return q + d + n + p;
}
// ────────────────────────────────────────────────────────────────────────────
// ─── TASK 3: fix changeOwed — cents of change after the customer overpays ───
// EXPECTED: amountPaid - price. e.g. changeOwed(100, 75) == 25.
// The operands look swapped. Which order makes change come out POSITIVE?
//
// >>> YOUR CODE HERE <<<
//
int changeOwed(int amountPaid, int price)
{
return price - amountPaid; // BUG?
}
// ────────────────────────────────────────────────────────────────────────────
// ─── TASK 4: fix rollsValue — value of `rolls` full rolls of quarters ───────
// EXPECTED: rolls * kQuartersPerRoll * kQuarter, and kQuartersPerRoll is 40.
// Someone hard-coded the count instead of using the named constant — and got
// it off by one. Prefer the constant from the header so it can never drift.
//
// >>> YOUR CODE HERE <<<
//
int rollsValue(int rolls)
{
int quartersInRolls { rolls * 39 }; // BUG?
return coinWorth(quartersInRolls, kQuarter);
}
// ────────────────────────────────────────────────────────────────────────────
// ─── TASK 5: fix machineCents — grand-total audit number ────────────────────
// EXPECTED: trayValue(quarters, dimes, nickels, pennies) + rollsValue(rolls).
// TWO things are wrong in this body:
// (a) the call to trayValue passes its arguments in the wrong ORDER — the
// header's parameter order is (quarters, dimes, nickels, pennies); and
// (b) the running total starts from a stray seed value instead of 0, so even
// a correct sum comes out too large (a "wrong initial value" bug — the
// local does not begin where you assumed; notes §3.8).
// Fix BOTH `// BUG?` lines.
//
// >>> YOUR CODE HERE <<<
//
int machineCents(int rolls, int quarters, int dimes, int nickels, int pennies)
{
int total { kQuarter }; // BUG?
int loose { trayValue(dimes, quarters, nickels, pennies) }; // BUG?
int rolled { rollsValue(rolls) };
return total + loose + rolled;
}
// ────────────────────────────────────────────────────────────────────────────
Try the lab first — the learning is in the attempt.
// coin_tray.cpp — Vending-Machine Coin-Tray Auditor (REFERENCE SOLUTION)
//
// All six planted logical bugs corrected. Each fix is annotated with the bug
// CLASS it belonged to and the expected-vs-actual reasoning that exposes it.
// Peek only after you've tried — the skill this chapter trains is the hunt
// itself, not the answers.
//
// Compiles warning-clean under -Wall -Wextra; passes every check in tests/.
#include "../coin_tray.h"
// TASK 1 — bug class: WRONG OPERATOR.
// Expected coinWorth(3, 25) == 75; the buggy `count + coinValue` gave 28.
// Value of N coins is a product, not a sum.
int coinWorth(int count, int coinValue)
{
return count * coinValue;
}
// TASK 2 — bug class: COPY-PASTE SLIP.
// The dimes line had been copied from the nickels line and still multiplied
// by kNickel (5) instead of kDime (10), so every dime was undercounted by 5c.
// Expected trayValue(2,1,0,3) == 50+10+0+3 == 63. The raw starter printed 42
// (this line's bug plus the upstream coinWorth bug); with coinWorth fixed but
// this line still wrong it would read 58 — either way, 5c short per dime.
int trayValue(int quarters, int dimes, int nickels, int pennies)
{
int q { coinWorth(quarters, kQuarter) };
int d { coinWorth(dimes, kDime) }; // fixed: kNickel -> kDime
int n { coinWorth(nickels, kNickel) };
int p { coinWorth(pennies, kPenny) };
return q + d + n + p;
}
// TASK 3 — bug class: SWAPPED OPERANDS.
// Subtraction is not commutative. `price - amountPaid` returns the NEGATIVE
// of the change. Expected changeOwed(100, 75) == 25; the bug gave -25.
int changeOwed(int amountPaid, int price)
{
return amountPaid - price;
}
// TASK 4 — bug class: OFF-BY-ONE CONSTANT.
// A roll holds 40 quarters, not 39. Using the named constant kQuartersPerRoll
// both fixes the value and prevents the number from drifting again.
// Expected rollsValue(1) == 1*40*25 == 1000. The raw starter printed 64 (the
// upstream coinWorth bug dominates); with coinWorth fixed but 39 still here it
// reads 975 == 39*25 — the off-by-one laid bare.
int rollsValue(int rolls)
{
int quartersInRolls { rolls * kQuartersPerRoll }; // fixed: 39 -> kQuartersPerRoll
return coinWorth(quartersInRolls, kQuarter);
}
// TASK 5 — bug classes: SWAPPED CALL ARGUMENTS + WRONG INITIAL VALUE.
// (a) trayValue's parameter order is (quarters, dimes, nickels, pennies);
// the buggy call passed dimes and quarters in the wrong slots, so the
// two were valued at each other's denomination.
// (b) the accumulator started at kQuarter (25) instead of 0, inflating every
// audit by 25c. Accumulators must start empty.
// Expected machineCents(1, 2, 1, 0, 3) == trayValue(2,1,0,3) + rollsValue(1)
// == 63 + 1000 == 1063.
int machineCents(int rolls, int quarters, int dimes, int nickels, int pennies)
{
int total { 0 }; // fixed: kQuarter -> 0
int loose { trayValue(quarters, dimes, nickels, pennies) }; // fixed: argument order
int rolled { rollsValue(rolls) };
return total + loose + rolled;
}
// tests.cpp — the automated grader for the Coin-Tray Auditor.
//
// This is the falsifiable "expected" side of the debugging story (notes §3.2):
// every CHECK is one row of the bug report — a concrete input with the answer
// the auditor is supposed to produce. A failing CHECK prints the offending
// expression and its line, which LOCALIZES the bug to one function. Read the
// FIRST failure first: fix the earliest divergence, rebuild, repeat.
//
// Tiny no-framework harness (same pattern as the other drills).
#include <iostream>
#include "../coin_tray.h"
static int fails { 0 };
#define CHECK(cond) \
do { if (!(cond)) { \
std::cerr << "FAIL: " #cond " @line " << __LINE__ << '\n'; \
++fails; \
} } while (0)
int main()
{
// ── coinWorth: a count of coins is a PRODUCT, not a sum ──────────────
CHECK(coinWorth(3, kQuarter) == 75); // bug report: "expect 75, got 28"
CHECK(coinWorth(4, kDime) == 40);
CHECK(coinWorth(1, kPenny) == 1);
CHECK(coinWorth(0, kQuarter) == 0); // edge: zero coins -> zero value
CHECK(coinWorth(7, 0) == 0); // edge: a zero-value coin
// ── trayValue: each denomination must use ITS OWN constant ───────────
CHECK(trayValue(2, 1, 0, 3) == 63); // bug report: "expect 63, got 42"
CHECK(trayValue(0, 1, 0, 0) == 10); // isolates the dime term alone
CHECK(trayValue(0, 0, 1, 0) == 5); // isolates the nickel term alone
CHECK(trayValue(0, 0, 0, 0) == 0); // edge: empty tray
CHECK(trayValue(4, 0, 0, 0) == 100); // four quarters == one dollar
// ── changeOwed: amountPaid - price, and it must be non-negative ──────
CHECK(changeOwed(100, 75) == 25); // bug report: "expect 25, got -25"
CHECK(changeOwed(50, 50) == 0); // edge: exact payment -> no change
// ── rollsValue: a roll is 40 quarters ($10.00) ──────────────────────
CHECK(rollsValue(1) == 1000); // bug report: "expect 1000, got 64"
CHECK(rollsValue(0) == 0); // edge: no rolls
CHECK(rollsValue(3) == 3000);
// ── machineCents: loose tray + rolled quarters, starting from zero ───
CHECK(machineCents(1, 2, 1, 0, 3) == 1063); // bug report: "expect 1063, got 131"
CHECK(machineCents(0, 0, 0, 0, 0) == 0); // edge: empty machine -> 0
CHECK(machineCents(0, 1, 1, 1, 1) == 41); // tray only: 25+10+5+1
CHECK(machineCents(2, 0, 0, 0, 0) == 2000); // rolls only: two $10 rolls
if (!fails)
std::cout << "PASS ✅ all checks — every planted bug is fixed.\n";
else
std::cerr << "FAIL ❌ " << fails
<< " check(s) failing. Fix the FIRST one, rebuild, repeat.\n";
return fails ? 1 : 0;
}
// coin_tray.h — the INTERFACE for the Vending-Machine Coin-Tray Auditor.
//
// This file is COMPLETE and CORRECT. Do not edit it. It contains only
// DECLARATIONS (the shape of each function) plus the four coin values. The
// bodies — where the bugs are hiding — live in starter/coin_tray.cpp.
//
// Read these declarations as a contract. Each comment states EXACTLY what the
// function is supposed to return. When you debug, that promise is your
// "expected"; the value the code actually produces is your "actual". The bug is
// always the first place those two disagree (see notes/chapter-03.md, §3.3).
#ifndef COIN_TRAY_H // header guard (Chapter 2.12): include this once
#define COIN_TRAY_H
// ─────────────────────────────────────────────────────────────────────────
// Coin denominations, in CENTS. These constants are correct — trust them.
// (constexpr constants are safe to DEFINE in a header: each translation unit
// gets its own private copy — unlike functions, which need one .cpp home.)
// ─────────────────────────────────────────────────────────────────────────
constexpr int kPenny { 1 };
constexpr int kNickel { 5 };
constexpr int kDime { 10 };
constexpr int kQuarter { 25 };
// A roll of quarters from the bank holds exactly 40 quarters ($10.00).
constexpr int kQuartersPerRoll { 40 };
// ─────────────────────────────────────────────────────────────────────────
// The auditor's functions. EXPECTED behavior is spelled out per function.
// ─────────────────────────────────────────────────────────────────────────
// EXPECTED: the value, in cents, of `count` coins each worth `coinValue` cents.
// coinWorth(3, kQuarter) -> 75 (three quarters = 75 cents)
// coinWorth(0, kDime) -> 0
int coinWorth(int count, int coinValue);
// EXPECTED: the total value, in cents, of a tray holding the given coin counts.
// = quarters*25 + dimes*10 + nickels*5 + pennies*1
// trayValue(2, 1, 0, 3) -> 50 + 10 + 0 + 3 = 63
int trayValue(int quarters, int dimes, int nickels, int pennies);
// EXPECTED: cents left to dispense as change after the customer overpays.
// = amountPaid - price (both already in cents; caller guarantees paid >= price)
// changeOwed(100, 75) -> 25
int changeOwed(int amountPaid, int price);
// EXPECTED: the value, in cents, of `rolls` full rolls of quarters.
// = rolls * kQuartersPerRoll * kQuarter
// rollsValue(1) -> 1 * 40 * 25 = 1000 ($10.00)
int rollsValue(int rolls);
// EXPECTED: total cents the machine holds = loose-tray value + rolled quarters.
// = trayValue(quarters, dimes, nickels, pennies) + rollsValue(rolls)
// This is the top-level audit number printed on the maintenance ticket.
int machineCents(int rolls, int quarters, int dimes, int nickels, int pennies);
#endif // COIN_TRAY_H
# Chapter 3 — Coin-Tray Auditor · unit-test grader (Style B).
# Targets follow the drills/CLAUDE.md Makefile contract. Recipes use TABS.
#
# The starter is a library of functions (no main of its own), so `build`
# COMPILES it (-c) to prove it has no syntax / compile-time errors — the whole
# point of the chapter is that a clean compile does NOT mean correct. `run`,
# `test`, and `test-solution` link the grader (tests/tests.cpp) against either
# the starter bodies or the reference bodies.
CXX := clang++
CXXFLAGS := -std=c++17 -Wall -Wextra
.PHONY: all build run test solution test-solution clean
all: build
# Compile-only: proves the starter builds warning-clean (no linked main needed).
build: starter/coin_tray.cpp coin_tray.h
$(CXX) $(CXXFLAGS) -c starter/coin_tray.cpp -o starter/coin_tray.o
# "Run" the starter == run the grader against it (a library's natural exercise).
run: test
# Your red->green loop: grade the STARTER bodies.
test: starter/coin_tray.cpp coin_tray.h tests/tests.cpp
@$(CXX) $(CXXFLAGS) tests/tests.cpp starter/coin_tray.cpp -o tests/run
@./tests/run
# Build + run the grader against the reference solution (for when you're stuck).
solution: solution/coin_tray.cpp coin_tray.h tests/tests.cpp
@$(CXX) $(CXXFLAGS) tests/tests.cpp solution/coin_tray.cpp -o tests/run
@./tests/run
# Proof the exercise is solvable: the reference bodies MUST pass every check.
test-solution: solution/coin_tray.cpp coin_tray.h tests/tests.cpp
@$(CXX) $(CXXFLAGS) tests/tests.cpp solution/coin_tray.cpp -o tests/run
@./tests/run
clean:
rm -f starter/coin_tray.o tests/run
make test locally
(see “Build & run locally” above).