Module 5 — Worked Examples¶
Every example asks you to predict before it shows you the output, because the
prediction is the exercise and the output is only the marking scheme. Where a
program prints more than is useful here, an excerpt is marked with ….
The programs are in examples/module-05/, each folder with its own
expected-output.txt. Build any of them with:
Example 1 — class-level state, and one counter for every object [L2 Apply]¶
The problem: tickets are issued with serial numbers, and the system must know how many are alive right now and what the next serial should be. Neither of those is a per-ticket fact.
Before you read on, predict: three tickets are created — first, second, and
a copy of second — then their scope ends. How many times was the constructor
called? The destructor? What does the "issued" count say afterwards?
// examples/module-05/01-static-members/main.cpp
class Ticket {
public:
static int liveCount() { return live_; }
static int totalIssued() { return nextSerial_ - kFirstSerial; }
explicit Ticket(std::string event)
: event_(std::move(event)), serial_(nextSerial_++) { ++live_; }
Ticket(const Ticket& other) // a copy is a NEW ticket
: event_(other.event_), serial_(nextSerial_++) { ++live_; }
~Ticket() { --live_; }
private:
static int live_;
static int nextSerial_;
inline static std::string venue_ = "Main Hall";
};
int Ticket::live_ = 0;
int Ticket::nextSerial_ = Ticket::kFirstSerial;
Output (excerpt):
Class-level state, read with no object in existence:
live: 0
issued: 0
Issuing three tickets, one of them a copy:
#1000 Orientation
#1001 Orientation
#1002 Orientation <- copied from #1001, but a different ticket
live now: 3
The objects are gone. The class-level state is not:
live: 0 <- the destructor decremented one shared counter, not three
issued: 3 <- and this only ever went up
Why it behaves that way: live_ and nextSerial_ have one storage location
for the entire program, not one per object, so ++live_ in three constructors
increments the same integer three times. Ticket::liveCount() can be called
before any Ticket exists, because class-level state is not created by the
objects — it is created by the program starting.
What to take away: a static data member is how a type holds a fact about the
whole class. It is also, unavoidably, shared mutable state: the comment "two
objects, one shared fact, and no way for them to disagree" is the benefit and the
warning in one line.
Example 2 — teaching a type the operators C++ already speaks [L2 Apply]¶
The problem: money that prints, compares and sorts. The design decisions here are the lesson, not the arithmetic.
Before you read on, predict: price is Rs 1250.50 and fee is Rs 20.00. What
does fee - price print, given that Money stores a long long of paisa and the
only arithmetic operators written are += and unary -? And how many of the six
comparisons were written by hand?
// examples/module-05/02-money-operators/main.cpp (the essential parts)
Money& operator+=(const Money& other) { paisa_ += other.paisa_; return *this; }
Money& operator-=(const Money& other) { return *this += -other; }
bool operator==(const Money& other) const { return paisa_ == other.paisa_; }
bool operator<(const Money& other) const { return paisa_ < other.paisa_; }
bool operator!=(const Money& other) const { return !(*this == other); }
bool operator>(const Money& other) const { return other < *this; }
bool operator<=(const Money& other) const { return !(other < *this); }
bool operator>=(const Money& other) const { return !(*this < other); }
Money operator-() const { return Money(-paisa_); }
Money operator+(Money left, const Money& right) { left += right; return left; }
Money operator-(Money left, const Money& right) { left -= right; return left; }
Output (excerpt):
Arithmetic - `+` is built on `+=`:
price + fee = Rs 1270.50
price - fee = Rs 1230.50
fee - price = -Rs 1230.50 (negative, and it still prints as money)
-price = -Rs 1250.50
price is untouched by all of it: Rs 1250.50
And `operator<` is enough for the standard library to sort:
sorted: Rs 0.01 Rs 20.00 Rs 99.50 Rs 1250.50 Rs 1250.50
Why it behaves that way: the free operator+ takes its left operand by
value — it is a copy about to be modified — and delegates to +=, so there is
one implementation of addition and no way for the two to disagree. The six
comparisons are two definitions plus four derivations, so they cannot drift. And
std::sort needed nothing at all: once < exists, the algorithm exists.
What to take away: write the fewest operators that mean the most. +=, ==,
< and << gave Money arithmetic, comparison, sorting and output — and the last
two arrived without writing any code that mentions Money.
Example 3 — input is harder than output, and >> can lie [L4 Analyze]¶
The problem: read records from text. Output is the easy half; input has a trap so precise that this example is built around it.
Before you read on, predict: the text below is read field by field. After the failed read on line 3, what does the object contain — the old values, or the fields that were successfully read before the failure?
// examples/module-05/03-stream-io/main.cpp — the naive version, and the fix
std::istream& operator>>(std::istream& in, Item& item) {
in >> item.id_ >> item.title_ >> item.pages_; // straight into the members
return in;
}
std::istream& operator>>(std::istream& in, SafeItem& item) {
int id = 0; // parse into locals first…
std::string title;
int pages = 0;
if (in >> id >> title >> pages) {
item = SafeItem(id, std::move(title), pages); // …and assign only on success
}
return in;
}
Output (excerpt):
--- read through Item (straight into the members) ---
line 3: FAIL (no more input) holds #1003 "BrokenLine" pages=0 <- invalid
--- read through SafeItem (parse into a local, assign on success) ---
line 3: FAIL (no more input) holds #0 "" pages=0 <- invalid
--- the case that actually hurts: a failed read into a healthy object ---
Item before : #7 AlreadyGood (300 pages)
Item after a FAIL : #8 Truncated (300 pages) <- id and title replaced
SafeItem after FAIL: #7 AlreadyGood (300 pages) <- untouched
Why it behaves that way: operator>> writes into the object as it goes, and a
failure halfway leaves the earlier writes in place. std::istream has no rollback:
the false it returns means "the extraction failed", not "nothing happened". The
fixed version applies Module 4's rule — a refused operation changes nothing — by
reading into locals and assigning only when every field arrived.
What to take away: an operation that can fail must leave its object in a known
state. This is the same rule as borrow() returning false without changing the
count, applied to a stream. It is also the reason operator>> usually needs
friend: it must write, and no accessor can help it.
Example 4 — the operators you should leave alone, measured [L4 Analyze]¶
The problem: && and || short-circuit — the right operand may not run at
all. That guarantee disappears the moment you overload them, and this program
counts how many times a function actually ran, so the loss is a number rather than
an assertion.
Before you read on, predict: missing is a search result that failed.
expensiveCheck() increments a counter and returns true. In missing &&
expensiveCheck(), how many times does the counter increment? And what would the
answer be if SearchResult had declared its own operator&&?
// examples/module-05/04-operators-not-to-overload/main.cpp
int checksRun = 0;
bool expensiveCheck() { ++checksRun; return true; }
class SearchResult {
public:
explicit SearchResult(bool found) : found_(found) {}
explicit operator bool() const { return found_; } // the SAFE conversion
};
Output (excerpt):
Short-circuiting still works, because `&&` was NOT overloaded:
missing && expensiveCheck() -> the right operand ran 0 time(s)
found && expensiveCheck() -> the right operand ran 1 time(s)
found || expensiveCheck() -> the right operand ran 0 time(s)
Why it behaves that way: && is not a function call. The language defines it
to evaluate the left operand, decide, and then decide whether the right operand
is needed at all. A user-defined operator&& is an ordinary function call, so
both operands are evaluated before it runs — the counter would read 1 in every
case, including the first.
What to take away: explicit operator bool is the one conversion an interface
should provide; it makes if (result) work without letting a search result turn
into an integer and join the arithmetic. And &&, ||, , and & should never
be overloaded: each one carries a guarantee — short-circuiting, ordering, taking an
address — that overloading destroys silently.
Example 5 — deciding member versus free, five times [L5 Design]¶
The problem: a Loan type needs to be comparable, printable, and to support
"extend by n days". Decide, for each, whether the operator is a member and why.
Design it yourself first, then compare:
| Proposal | Verdict | Reason |
|---|---|---|
bool operator==(const Loan&, const Loan&) |
free | lets either operand be converted; symmetric by construction |
bool operator<(const Loan&) const |
member | no conversion is wanted, and due < other.due reads honestly |
std::ostream& operator<<(std::ostream&, const Loan&) |
free, mandatory | the left operand is std::ostream |
Loan& operator+=(int days) |
member | it modifies *this |
Loan operator+(Loan, int days) |
free | 30 + loan should not be expressible, but a free + built on += guarantees the two agree |
void operator,(const Loan&) const |
do not write it | sequencing is a language guarantee, and the API becomes unreadable |
bool operator==(const Loan&, int id) |
free, but think again | comparing a loan with an integer invites if (loan == 5), which nobody meant; name the function |
What to take away: the decisions are not stylistic. Each row above has a reason you can state in one sentence, and the reason is what you will be asked for in a design review — not the choice.
What to do with this page¶
- Build and run all four programs. They are verified against their captured output, so a difference means something about your toolchain, and it is worth knowing what.
- Then break one. Add a
<=to the comparator in02-money-operatorsand see whatstd::sortdoes. That deliberate failure is Lab 5's required deliverable, and it is far more convincing from your own terminal. - Predict before you run, every time. The prediction is what the exercise is for.
Next: Misconceptions and mistakes — eight wrong mental models, and the errors with their real messages. · All modules