Skip to content

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:

g++ -std=c++17 -Wall -Wextra main.cpp -o program && ./program

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?

1001 C++Primer 976
1002 CleanCode 464
1003 BrokenLine
ABC Malformed 464
// 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

  1. 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.
  2. Then break one. Add a <= to the comparator in 02-money-operators and see what std::sort does. That deliberate failure is Lab 5's required deliverable, and it is far more convincing from your own terminal.
  3. 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