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
staticcount of how manyMoneyvalues exist, with a static query, and a demonstration that it moves as values are created and destroyed; - a
staticnamed constructor reading from text, soMoney::from_text("12.34")is possible; percent(int)— a percentage of the amount, with a documented rounding rule;split(int parts)— the amount divided intopartswhole 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::cinandstd::istringstream. - Use a
staticmember 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¶
- Every value is a whole number of pence;
doubleappears nowhere (search for it and record the result). - All arithmetic is exact:
(a + b) - b == afor every test value. - Division by zero is refused with a message, and does not crash.
split(parts)returns values that sum exactly to the original, and refusesparts <= 0; the remainder distribution rule is documented and demonstrated.percentrounds half away from zero and documents that choice.<<prints exactly two decimal places, with a minus sign only when the value is negative — and never prints-0.00.>>refuses malformed input by setting the stream's fail state, and reads nothing on failure.- Relational operators are consistent: exactly one of
a < b,a == b,a > bis true for any pair. - The
staticcounter is exact after a scope ends, and its query is a static function. - No operator mutates its operand except the compound assignments.
Constraints¶
- Header plus source file for
Money, and amain.cpp; build with an explicit file list. - The value is private; there is no public setter.
friendis 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:
- For each operator in the table, is it a member or a free function? Give the rule you are using.
- Which operators return a new value, and which return a reference? Say what breaks if you confuse them.
a * 3and3 * a— how do you make both work, and which one has to be a free function?- What does
/mean for money? Write the two candidate rules and choose one. - Write the operator you will not provide, with a one-sentence reason a reviewer would accept.
Implementation tasks¶
- Implement the value, the constructors and the static counter.
- Implement arithmetic and the compound assignments, then the relational set in terms of
<and==. - Implement the stream operators, testing
<<onstd::coutand>>on anstd::istringstream. - Implement
percentandsplitwith their documented rules. - Write
mainthat runs every test case and prints a labelled result line for each. - Run under
-Wall -Wextra -Werrorand 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:
- Which operator is wrong, and which pair of values exposes it?
- Why does a sort with this comparator produce an order that is not an order?
- Rewrite the set so that
<=,>,>=and!=are each defined from the two primitives, and say why that is safer than writing seven independent comparisons. - What would
std::sortdo with your broken comparator at scale, and which warning flag (if any) would have helped?
Reflection questions¶
- Which operator was hardest to get exactly right, and what made it hard?
>>refuses malformed input by setting the fail state. What would a different design (throwing, or returning a flag) cost the caller?- The static counter moved as values were created. Which of your operations create unnamed values, and how did you notice them?
- Which operator do you now believe is missing from your type after using it in
main, and why did you not add it? splitdistributes a remainder. What is the rule, and would an accountant accept it?- 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 -Werrorand no warnings - [ ] No
doublein the implementation — searched for and recorded - [ ] All arithmetic exact, checked by the round-trip test over many pairs
- [ ] Division by zero refused without crashing
- [ ]
splitandpercenthave 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.mdnames 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