Skip to content

Practice Lab 9 — Money type with full operator support

Difficulty: Intermediate · Estimated time: 4–5 hours · Module 5: Static Members, Friend Functions and Operator Overloading

The type that every other lab in this course would like to have. Get it right once and the arithmetic stops being a source of bugs.


At a glance

Difficulty Intermediate
Estimated time 4–5 hours
Module Module 5 — Static Members, Friend Functions and Operator Overloading
Concepts practised Operator overloading (arithmetic, comparison, compound assignment, stream insertion and extraction), friend functions, static members, when not to overload, value semantics
Prerequisite Practice Lab 8
Class design given — one value type, with the operator set specified

Scenario

A campus shop has three programs that each keep money as a double: the till, the invoice printer and a repayment tracker. Three programs do not agree to the penny, and the shop's accountant has banned double from the building.

You are writing the Money type the three programs will share. It must behave like a number where that is honest, and refuse where it would be a lie.


Problem statement

Implement an immutable value type Money holding a whole number of pence, with this operator set:

Operator Meaning Notes
+, - addition, subtraction same currency only
unary - negation
+=, -= compound assignment
* with an int scaling by a whole number both orders
/ with an int splitting into whole parts must define and document the remainder rule
==, !=, <, <=, >, >= comparison <= derived from < and ==
<< stream insertion 1234 prints as 12.34, -5 as -0.05, 0 as 0.00
>> stream extraction accepts 12.34, 12, .5; refuses the rest

Plus:

  • a static count of how many Money values exist, with a static query, and a demonstration that it moves as values are created and destroyed;
  • a static named constructor reading from text, so Money::from_text("12.34") is possible;
  • percent(int) — a percentage of the amount, with a documented rounding rule;
  • split(int parts) — the amount divided into parts whole amounts that still add up to the original.

You must also write, in operator-notes.md, one operator you deliberately did not provide, and why. Multiplying two Money values, adding a Money to a double, and using == on floating-point approximations are the usual candidates.


Learning objectives

By the end of this lab you will be able to:

  • Decide which operators a type genuinely supports, and defend leaving others out.
  • Implement arithmetic and comparison operators with consistent semantics.
  • Implement stream insertion and extraction so a type works with std::cout, std::cin and std::istringstream.
  • Use a static member for class-level state, and explain its lifetime and its sharing.
  • Recognise an operation that cannot be made exact, and define its rounding rule explicitly.

Functional requirements

  1. Every value is a whole number of pence; double appears nowhere (search for it and record the result).
  2. All arithmetic is exact: (a + b) - b == a for every test value.
  3. Division by zero is refused with a message, and does not crash.
  4. split(parts) returns values that sum exactly to the original, and refuses parts <= 0; the remainder distribution rule is documented and demonstrated.
  5. percent rounds half away from zero and documents that choice.
  6. << prints exactly two decimal places, with a minus sign only when the value is negative — and never prints -0.00.
  7. >> refuses malformed input by setting the stream's fail state, and reads nothing on failure.
  8. Relational operators are consistent: exactly one of a < b, a == b, a > b is true for any pair.
  9. The static counter is exact after a scope ends, and its query is a static function.
  10. No operator mutates its operand except the compound assignments.

Constraints

  • Header plus source file for Money, and a main.cpp; build with an explicit file list.
  • The value is private; there is no public setter.
  • friend is allowed only for the operators that cannot be members (stream operators), and you must justify each one in the notes.
  • No inheritance, no templates, no smart pointers.
  • The relational set must be implemented in terms of one another and == — no seven independent comparisons.
  • No warnings, including no unused-parameter warnings.

Starter task

In operator-notes.md, before implementing:

  1. For each operator in the table, is it a member or a free function? Give the rule you are using.
  2. Which operators return a new value, and which return a reference? Say what breaks if you confuse them.
  3. a * 3 and 3 * a — how do you make both work, and which one has to be a free function?
  4. What does / mean for money? Write the two candidate rules and choose one.
  5. Write the operator you will not provide, with a one-sentence reason a reviewer would accept.

Implementation tasks

  1. Implement the value, the constructors and the static counter.
  2. Implement arithmetic and the compound assignments, then the relational set in terms of < and ==.
  3. Implement the stream operators, testing << on std::cout and >> on an std::istringstream.
  4. Implement percent and split with their documented rules.
  5. Write main that runs every test case and prints a labelled result line for each.
  6. Run under -Wall -Wextra -Werror and fix every warning rather than silencing it.

Test cases

# Action Expected result
1 1234 << 12.34
2 -5 << -0.05 (not -0.05 from 0.05 being negative — check both signs)
3 0 << 0.00
4 "12.34" >> 1234 pence
5 "12" >> 1200 pence
6 ".5" >> 50 pence
7 "12.3.4" >> refused; stream fail state set; value untouched
8 (a + b) - b equals a for 100 random pairs you generate yourself
9 a / 0 refused, with a message; no crash
10 split(100) of 100 pence into 3 parts Three parts summing exactly to 100, remainder rule visible
11 percent(15) of 1234 pence The documented rounding, checked by hand
12 Relational consistency over 20 pairs Exactly one of <, ==, > is true for each pair
13 Static counter after a scope exits Returns to the previous value

Boundary cases to run deliberately: 1 penny; a value that would print -0.00 if the sign were naive; split into 1 part; split into more parts than pence.


Expected behaviour

Money a = 1234 pence, printed: 12.34
Money b = -5 pence, printed: -0.05
a + b printed: 12.29
a < b: false    a == b: false    a > b: true
0.00 printed for a zero value: 0.00
from_text("12.34") -> 1234 pence
from_text("12.3.4") -> refused: '12.3.4' is not a money value
a / 0 -> refused: cannot divide money by zero
split 100 pence into 3: 34 + 33 + 33 = 100
percent(15) of 1234 pence -> 185 pence (rounded half away from zero)
values alive now: 3

Extension challenge

Add currency to Money, so it carries which currency it is in.

Record in operator-notes.md:

  • Which operations now have to check that both operands are the same currency, and what they do when they are not (refuse, throw, or convert?).
  • Why + on two different currencies has no obvious meaning, and what a design that does support it would need.
  • Whether the comparison operators can stay consistent once currencies exist, with an example.
  • The change cost: how many operators, and how many lines.

Debugging challenge

This comparison set is inconsistent, and the bug appears only in a sort:

bool operator<(const Money& o) const { return pence_ <  o.pence_; }
bool operator<=(const Money& o) const { return pence_ <= o.pence_; }
bool operator==(const Money& o) const { return pence_ == o.pence_; }
bool operator>=(const Money& o) const { return !(*this < o); }

Reproduce it — find a pair of values for which two of the three relations in the table's case 12 are simultaneously true — and answer:

  1. Which operator is wrong, and which pair of values exposes it?
  2. Why does a sort with this comparator produce an order that is not an order?
  3. Rewrite the set so that <=, >, >= and != are each defined from the two primitives, and say why that is safer than writing seven independent comparisons.
  4. What would std::sort do with your broken comparator at scale, and which warning flag (if any) would have helped?

Reflection questions

  1. Which operator was hardest to get exactly right, and what made it hard?
  2. >> refuses malformed input by setting the fail state. What would a different design (throwing, or returning a flag) cost the caller?
  3. The static counter moved as values were created. Which of your operations create unnamed values, and how did you notice them?
  4. Which operator do you now believe is missing from your type after using it in main, and why did you not add it?
  5. split distributes a remainder. What is the rule, and would an accountant accept it?
  6. Which of your operators could silently change the meaning of existing code if you changed its semantics later, and how would a reader find out?

Completion checklist

  • [ ] It builds from a documented command with -Wall -Wextra -Werror and no warnings
  • [ ] No double in the implementation — searched for and recorded
  • [ ] All arithmetic exact, checked by the round-trip test over many pairs
  • [ ] Division by zero refused without crashing
  • [ ] split and percent have documented, demonstrated rounding rules
  • [ ] << and >> both work, and malformed input sets the fail state
  • [ ] Relational operators consistent over a table of pairs
  • [ ] The static counter is exact after a scope exits, and queried through a static function
  • [ ] operator-notes.md names one operator deliberately not provided, with a reason
  • [ ] All thirteen test cases run, with output recorded
  • [ ] The debugging challenge was reproduced, explained, and only then fixed

Related: Lab index · Module 5 — Operators and Static Members · Previous: Practice Lab 8 · Next: Practice Lab 10 — Warehouse bin with class-level barcodes