Skip to content

Module 2 — Worked Examples

Five programs, in the order the module needs them. Each one is real: it builds and runs with the course's command, and its captured output sits beside it in examples/module-02/.

The rule for this page. Before each program's output, predict it in writing. Not "roughly" — the actual lines. Then run it and account for every difference. The prediction is the exercise; the output is only the marking scheme. This habit is what turns reading code into being able to write it.


Example 1 — the first class: a type that owns its behaviour

examples/module-02/01-book-class/main.cpp

The data is deliberately the same kind of thing as Module 1's parallel arrays: an identifier, a title, a page count, and a question — is this book a long read? What changed is where the rules live.

class Book {
public:
    Book(int id, const std::string& title, int pages)
        : id_(id), title_(title), pages_(pages) {
    }

    void describe() const { ... }
    bool hasId(int id) const { return id_ == id; }
    bool isLongRead() const { return pages_ > 500; }
    void correctPagesTo(int pages) { if (pages > 0) { pages_ = pages; } }

private:
    int id_;
    std::string title_;
    int pages_;
};

Four decisions worth naming, because you will make all four again in every class you write from now on:

  • hasId exists instead of int id() const. The caller wanted to know "is this the item with that identifier?", not what the identifier is. Answering the actual question means no caller ever compares identifiers itself.
  • isLongRead is a method, not a stored bool. The rule "> 500 pages" lives in one place. If the library decides 400 is long, exactly one line changes.
  • correctPagesTo checks its argument. It is the class's only way to change a page count, and it refuses nonsense. Compare with Module 4, where this becomes the central subject: a setter that accepts anything is barely better than a public field.
  • describe() is const; correctPagesTo() is not. The compiler now enforces the difference between asking and changing.

Now predict. main creates two books, corrects the second, and asks a question about each:

const Book primer(1001, "C++ Primer", 976);
Book clean(1003, "Clean Code", 464);
clean.correctPagesTo(500);

Predict: after correctPagesTo(500), what does clean.isLongRead() return, and does primer change at all? Write both answers down.

Output (run it first)
Two objects of the same type:
  #1001  C++ Primer  (976 pages)
  #1003  Clean Code  (464 pages)

Two objects, two independent sets of data:
  after correcting the second book:
  #1001  C++ Primer  (976 pages)
  #1003  Clean Code  (500 pages)

Asking an object a question about itself:
  does either book have id 1001? yes
  is the corrected book a long read? no longer

The two things to carry forward:

  1. primer is untouched. Two objects of one class hold two independent sets of data — this is ordinary variable semantics, and it is the answer to "where do the values go?".
  2. clean.pages_ is now exactly 500, and isLongRead() is false, because the rule is > 500. If you predicted "yes", you learned something more useful than if you predicted correctly: the boundary was checked in one place, and you were reasoning about it in another. That gap is exactly what the method exists to close.

Now break it deliberately. Add this line to main:

std::cout << primer.id_ << '\n';
error: 'int Book::id_' is private within this context

Keep the error message. It is the compiler stating that main is not allowed to depend on how a book stores its identifier — which is precisely what makes it safe to rename id_ tomorrow.


Example 2 — const member functions: who may call what

examples/module-02/02-const-members/main.cpp

A Loan that can be renewed, and a free function that may only look at one:

void printSummary(const Loan& loan) {     // no copy, no permission to modify
    std::cout << "  " << loan.summary() << '\n';
}

Predict, before reading on: three calls follow — a fresh loan, the same loan after renew(7), and a const Loan. Which of them can printSummary accept? What does the program print for the third?

Output (run it first)
Fresh loan:
  loan of 14 days, renewed 0 time(s)

After one renewal:
  loan of 21 days, renewed 1 time(s)

A const loan can still be read:
  days allowed: 30, renewals: 0

A temporary works too, because the function only reads:
  loan of 3 days, renewed 0 time(s)

Three lessons, in order of how often they matter:

1. const is contagious in exactly the right way. printSummary takes const Loan&, so inside it only const member functions may be called. That is why summary() is marked const. Remove the const from summary() and the caller stops compiling:

error: passing 'const Loan' as 'this' argument discards qualifiers

The error is in printSummary, but the fix belongs in summary(). This is the single most confusing error in the module, and §7 of Concepts walks through it. Read the message as: "you asked me to call a possibly-modifying function on an object I was told not to modify."

2. A const object is still useful. fixed can be read for the rest of its life. It just cannot be renewed. Marking an object const is a design statement — "this loan's terms are settled" — and the compiler enforces it.

3. A temporary can be passed to a function that only reads. printSummary(Loan(3)) is fine. The temporary lives until the end of the full expression that created it, which is longer than the call. Module 3 makes that rule precise.

Why you should care beyond this module. Everything that takes your type by const& is relying on this. std::cout << will want your type's output function to be const (Module 5). Standard containers require it (Module 14). A class that is not const-correct is a class that cannot be used in the places that matter.


Example 3 — this: two situations, and only two

examples/module-02/03-this-pointer/main.cpp

Book& withPages(int pages) {
    pages_ = pages;
    return *this;              // the object itself, not a pointer to it
}

Predict: this program calls

book.withPages(976).tagged("reference").setTitle("C++ Primer (5e)");

How many objects exist afterwards? What happens to other, the second book?

Output (run it first)
As created:
  #1001  C++ Primer  (0 pages)

After setTitle:
  #1001  C++ Primer, 5th edition  (0 pages)

Chained calls - each returns *this, which is the object:
  #1001  C++ Primer (5e)  [reference]  (976 pages)

A second object, unaffected by the chain:
  #1002  Clean Code  (0 pages)
  the two objects are distinct: 1

One object. Not three, not a copy per call. The chain works because each function returns Book& — a reference to the same object — so each successive call operates on exactly the object the first one did.

The &book != &other line prints 1 (true). It is there to show that comparing addresses is not the same as comparing books, and to leave you mildly annoyed that book == other does not compile at all. What should == mean for two books, and who should write it? That is Module 5, and finding it annoying now is the point.

The name-hiding situation is in the same file, commented:

void setTitle(const std::string& title) {
    title = title;          // ✗ assigns the parameter to itself — compiles, does nothing
}
void setTitle(const std::string& title) {
    this->title = title;    // ✓ now it means the member
}

The course's own style avoids the collision entirely by naming members with a trailing underscore. Both conventions are common in real code; recognise both, and if you use this-> everywhere, know that you are following a convention rather than solving a problem.


Example 4 — two objects, one conversation

examples/module-02/04-objects-interacting/main.cpp

A Course with room for three students, and four Students who want in. The design question is the section's whole content: which object owns the capacity rule?

bool Student::enrolIn(Course& course) {
    if (courseCount_ >= kMaxCourses) {
        return false;                 // the Student's rule
    }
    if (!course.reserveSeat(name_)) {
        return false;                 // the Course's rule
    }
    courses_[courseCount_] = course.title();
    ++courseCount_;
    return true;
}

Predict: Dave is the fourth student to try C++ Programming, which holds three. What does the program print for Dave, what does dave.describe() say, and what happens to Physics I at the end?

Output (run it first)
Two courses, four students, no enrolments yet:
  C++ Programming: 0/3  (empty)
  Calculus I: 0/3  (empty)

Alice enrols in both courses:
  C++ Programming: ok
  Calculus I:      ok
  Alice is enrolled in 2 course(s):  C++ Programming  Calculus I

Three more students try the same course:
  Bob:   ok
  Carol: ok
  Dave:  refused   <- the Course refused; Dave never saw the number 3
  C++ Programming: 3/3  Alice  Bob  Carol
  Dave is enrolled in 0 course(s):  none

The Student has a rule of its own, enforced somewhere else:
  Bob is enrolled in 2 course(s):  C++ Programming  Calculus I
  Alice's third course:  ok
  Alice's fourth course: refused   <- refused by Alice's limit of 3, not by Physics I, which is empty
  Alice is enrolled in 3 course(s):  C++ Programming  Calculus I  Computer Networks

A by-value parameter is a separate object:
  the copy arrives with 0 enrolled
  the copy leaves with 3 enrolled
  the original is untouched:
  Physics I: 0/3  (empty)

  (that copy filled up and was then destroyed on return;
   Physics I itself never held a single student)

Three things this output proves, and one it warns about.

Proves: the refusal is total. Dave's Student is unchanged (0 courses) and the Course never recorded him. Two objects, no possibility of disagreeing, because the Student's record is written only after the Course says yes.

Proves: two rules, two owners. Alice's fourth enrolment is refused by her limit of three courses — while Physics I is completely empty and willing. Neither class knows the other's number.

Proves: a by-value parameter is a separate object. fillWithoutAsking(Course copy) takes a copy, fills all three of its seats, and returns. The copy is destroyed, and Physics I still reports 0/3. The function did real work on an object nobody can observe afterwards.

Warns: that function is a plain bug in almost any real program. It compiles cleanly, does something plausible, and has no effect on the caller. How much of the program was copied? When exactly was the copy destroyed? Module 3 answers both, precisely, and Module 10 shows how to prevent it.


Example 5 — one type, two files, one command

examples/module-02/header-source/{Book.h,Book.cpp,main.cpp}

g++ -std=c++17 -Wall -Wextra main.cpp Book.cpp -o book_demo
./book_demo

Three files. Book.h declares what a Book is; Book.cpp defines what its functions do; main.cpp uses it and has never seen Book.cpp.

Predict — and then actually try it: delete Book.cpp from that command. What happens?

Answer (try it first) It still **compiles** — and then fails to **link**:
undefined reference to `Book::describe() const'
`main.cpp` compiled perfectly, because it saw a declaration: a promise that `Book::describe` exists somewhere. The linker then went looking and found nobody who had defined it. This is the most important distinction in the module's mechanics, and why Module 1 spent a whole section on it. The fix is to add `Book.cpp` back to the command. In Module 16 you will stop typing file lists by hand and let a build tool do it — but you cannot debug a build system you do not understand, which is why you are typing it now.

What to look at in these three files. Book.cpp includes Book.h, and its functions are written Book::describe() — the scope qualifier that says which class a function belongs to and brings Book's members into scope inside the body. Book.h includes only <string>, because that is all its declarations mention, even though describe() prints; the <iostream> include lives in Book.cpp, where printing happens. That is the rule from §8 of Concepts applied.

Build all three lines of a header/source pair by hand once

It is the last time in the course you will need to. From Lab 2 onwards your programs have more than one file, and knowing which file was missing from the command — rather than guessing — is what the next page (Debugging) teaches.


Example 6 — designing a type from nothing

examples/module-02/05-bank-account/main.cpp

The first five examples read. This one is designed, using the order from §3 of Concepts, and it is the shape Lab 2 asks for.

Step 1 — the invariant, in one sentence.

The balance is never negative, and every operation that changes it states its own precondition; a refused operation changes nothing.

Step 2 — the operations a bank program actually needs.

Operation Why the caller needs it
open an account with a balance you cannot create one any other way
deposit(paisa) money in
withdraw(paisa) money out
balanceText() display it
holder() identify it

Ask the question that matters: is there a setBalance in that table? No — and that is a design decision, not an omission. A caller who could set the balance could break the invariant in one line.

Step 3 — the representation those operations need.

private:
    std::string holder_;
    long long balancePaisa_;
};

Two members, and one of them carries a decision worth noticing: money is stored as an integer number of paisa, not as a double. A double cannot represent most decimal fractions exactly, so it accumulates small errors — acceptable in a physics simulation, not in a bank balance.

Predict: the program opens an account with Rs 2500.00, deposits Rs 1250.50, then tries to withdraw Rs 4000.00. What is the balance afterwards, and what does the withdrawal return?

Output (run it first)
Account opened:
  Ayesha: Rs 2500.00

Deposits:
  Rs 1250.50 -> ok, balance Rs 3750.50
  Rs 0.00    -> refused, balance Rs 3750.50   <- not a deposit
  Rs -50.00  -> refused, balance Rs 3750.50   <- nor is this

Withdrawals:
  Rs 4000.00 -> refused, balance Rs 3750.50   <- the balance would have gone negative
  Rs 3750.50 -> ok, balance Rs 0.00

The invariant still holds, and the object is still usable:
  Ayesha: Rs 1.00

A second account is a second object:
  Ayesha: Rs 1.00
  Bilal: Rs 5.00

The refused withdrawal changed nothing — that is what "a refused operation changes nothing" means in practice, and it is the property every well-designed type has and almost no badly-designed one does.

Now the uncomfortable question, which is Module 4's whole subject. This class has no setter and still the invariant is only mostly safe. Find the gap:

BankAccount(std::string holder, long long openingPaisa)
    : holder_(std::move(holder)), balancePaisa_(openingPaisa) {
    if (openingPaisa < 0) {
        balancePaisa_ = 0;      // silently "fixes" a negative opening balance
    }
}

The constructor pretends a negative opening balance was zero. A caller who passed -50000 has been told nothing, and the object that exists is not the object they asked for. The honest answers are refuse to construct (Module 13, exceptions) or make such a type impossible (Module 4, invariants). Nobody who has ever debugged a silent correction will prefer it to an error.


What to do with this page

  1. Type out each example yourself — do not copy files. Typing is where the syntax sticks, and it is how you will meet the small errors.
  2. Predict the output of each one in writing before running it.
  3. Break each program once, in a way this page suggests, and keep the error message.
  4. Then take the three steps of Example 6 and design a Vehicle or an EmployeeRecord — invariant, operations, representation — on paper, before opening a text editor. Lab 2 starts exactly there.

Next: Misconceptions and mistakes — the six wrong mental models, and the errors everyone makes. · All modules