Skip to content

Module 3 — Worked Examples

Four programs, in the order the module needs them, plus one analysis with no program at all. Each is real: it builds and runs with the course's command, and its captured output sits beside it in examples/module-03/.

The rule for this page, and it matters more here than anywhere else in the course: write down your prediction before running anything. Constructor and destructor traces are the one place where almost everybody is wrong the first time — and being wrong, in writing, is how you find out which part of your model is not the language.

If you predicted correctly on all four, move to the lab. If you did not, you have found the module's actual content.


Example 1 — when objects begin and end

examples/module-03/01-lifecycle-trace/main.cpp

A Tracked class that prints one line per construction, copy and destruction, a static counter of live objects, and a main with four nested scopes.

class Tracked {
public:
    explicit Tracked(std::string name) : name_(std::move(name)) {
        ++live_;
        std::cout << "+ " << name_ << "   (live: " << live_ << ")\n";
    }
    Tracked(const Tracked& other) : name_(other.name_ + " (copy)") { /* … */ }
    ~Tracked() {
        --live_;
        std::cout << "- " << name_ << "   (live: " << live_ << ")\n";
    }
private:
    std::string name_;
    static int live_;
};

Predict all of it. The trace has four blocks: two objects, a by-value call, a nested scope, and the end of main. For each block, write the lines you expect, including the live counter values. Then run it.

Output (run it first)
--- entering the outer scope
+ first   (live: 1)
+ second   (live: 2)

--- calling consumeByValue(second)
c second (copy)   (live: 3)
  consumeByValue received 3 live object(s)
- second (copy)   (live: 2)

--- opening a nested scope
+ inner   (live: 3)
  (inner exists only inside these braces)
- inner   (live: 2)
--- the nested scope has ended

--- ending the outer scope (watch the order)
- second   (live: 1)
- first   (live: 0)

Four lessons, and the first two are the ones people miss:

1. A by-value parameter is a copy, and it is destroyed at the end of the function. consumeByValue(second) produced a construction, a destruction, and a counter that went 2 → 3 → 2. The function took one parameter and created a whole extra object. Count what your own functions are doing: the surprising thing is not the copy, it is that the program contains more objects than the source appears to.

2. Destruction is the exact reverse of construction, per scope. second before first, because first was constructed first. Within one scope this is guaranteed. The rule generalises: members die in reverse declaration order, then the object itself.

3. + and - are balanced: the counter ends at exactly 0. Every construction has exactly one destruction. Get into the habit of reading a trace this way — a counter that ends anywhere other than 0 means an object was constructed once and destroyed twice, or never, and both of those are bugs you will meet in Module 10.

4. Nothing was copied at the end of main. The two objects were destroyed directly. That is the normal case, and it is worth noticing because it is normal: copies are not happening all the time, they are happening at the four or five places this module teaches you to look for.

Run it under a different standard

This program also builds under C++14: g++ -std=c++14 -Wall -Wextra -Werror main.cpp -o trace14. Do it and compare — the trace is byte-for-byte identical, because nothing here returns an object by value. The standard version does change a trace when a function returns one, which is why Lab 3's scenario includes makeItem, and why the course fixes -std=c++17 in every command rather than leaving it to chance.


Example 2 — the initialiser list, measured

examples/module-03/02-initialiser-lists/main.cpp

Two versions of the same class, differing only in where the members get their values. A Slot member type announces everything that happens to it.

class ByAssignment {
public:
    ByAssignment(int amount, int quantity) : amount_(0), quantity_(0) {
        amount_ = amount;          // the member already existed; this assigns to it
        quantity_ = quantity;
    }
private:
    Slot amount_;
    Slot quantity_;
};

class ByInitialiserList {
public:
    ByInitialiserList(int amount, int quantity) : amount_(amount), quantity_(quantity) {}
private:
    Slot amount_;
    Slot quantity_;
};

Predict: how many Slot: lines does each class print, and what are they? The totals are identical (total: 100 for both) — so what exactly is version 2 saving?

Output (run it first)
Version 1 - assigning in the constructor body:
    Slot: constructed with 0
    Slot: constructed with 0
    Slot: assigned 4 (after construction)
    Slot: assigned 25 (after construction)
  total: 100

Version 2 - the member initialiser list:
    Slot: constructed with 4
    Slot: constructed with 25
  total: 100

Initialisation order follows the declarations:
    3 x 4 = area 12

A const member, and a delegating constructor:
  t1: LIB-2026-0001
  t2: UNASSIGNED

Four lines against two — for an int-sized member. The saving in this toy example is invisible, which is the point: it is not about speed. The list is required, not preferred:

  • A const member cannot be assigned in the body at all. Try it and the compiler says uninitialized const member in 'const int' and assignment of read-only member. The Ticket::code_ member in this program is const, and the initialiser list is the only reason it can exist.
  • Neither can a reference member. Same shape of error: uninitialized reference member in 'int&'.
  • Neither can a member whose type has no default constructor. The compiler reports no matching function for call to 'Slot::Slot()', because assignment in the body requires the member to have been built first — and it cannot be.

So the habit is: initialise everything in the list, and use the body for logic that cannot be expressed as an initialiser (a validation throw, a loop, a log line). It costs one line to be right and nothing to be wrong, right up until it costs you an evening.

The third block is the declaration-order rule: area_(width_ * height_) works only because area_ is declared after width_ and height_, so it is initialised last. Reorder the declarations and the same line initialises area_ from two members that do not exist yet. The compiler warns (-Wreorder, three lines of it); the course's own build treats that warning as an error, for exactly this reason.

The fourth block shows both a const member and a delegating constructor: Ticket() does not initialise anything itself, it hands the work to Ticket("UNASSIGNED").


Example 3 — temporaries, copies, and two keywords that refuse

examples/module-03/03-copies-and-temporaries/main.cpp

A Session class with explicit on its constructor, a copy constructor that announces itself, and operator= deleted.

Predict three things before running it: how long the first temporary lives; how many times alice is constructed in section 2; and what happens at the end of main — how many objects are destroyed, and in what order.

Output (run it first)
1. A temporary, bound to a const reference:
  constructed for guest-token
  still alive: guest-token  (the reference keeps it alive)
  destroyed guest-token
  ... and now it is gone, at the end of the block

2. Passing by value makes a copy:
  constructed for alice
  copied
  inspectByValue is looking at alice (duplicated)
  destroyed alice (duplicated)
  the original is untouched: alice

3. One copy, two objects, both destroyed:
  destroyed alice

Section 1 — lifetime extension is real. Session("guest-token") is a temporary. Bound to const Session& temporary, it stays alive for the whole block, and is destroyed at the closing brace — not at the end of the line. Without the reference binding it would have died before the next statement. This is the one place a temporary outlives its full expression, and it works only because of the const.

Section 2 — one extra object, and the original is untouched. inspectByValue's parameter is a copy; the copy is destroyed before the function returns; alice never changed. Note the copy's name: alice (duplicated), chosen by the copy constructor. The copy constructor sees the source object and decides everything about the new one — which in Module 10 is precisely where a class either manages its resources correctly or doubles them up.

Section 3 — two objects, destroyed in reverse. alice is destroyed after the copy that was made in section 2 — but the copy was already destroyed at the end of that section, so the trace ends with a single line. This is the same "count the objects" discipline as Example 1: at the moment section 3 began, there was exactly one Session alive.

Now the three lines that do not compile. Each is in the file, commented, with its message. Uncomment them one at a time — this is the part of the example worth doing slowly, because each is a different mechanism refusing you:

Session implicit = "bob";
// error: conversion from 'const char [4]' to non-scalar type 'Session' requested

explicit refused a conversion the compiler would otherwise have performed silently.

Session other("carol");
alice = other;
// error: use of deleted function 'Session& Session::operator=(const Session&)'

Assignment was deleted on purpose. Note that copy-construction still works in this class — the two are separate decisions, and this example deliberately allows one and forbids the other.

Session noName;
// error: no matching function for call to 'Session::Session()'

One constructor was written, so the compiler generated no default one. §2 of Concepts — and the single most common error when a class gains its first constructor.


Example 4 — several constructors, and who runs first

examples/module-03/04-constructor-overloads/main.cpp

Four constructors, three of which delegate. Each prints from its body.

Predict: for const Loan carol("carol");, which constructors run, and in what order? Which line is printed first?

Output (run it first)
1. Two arguments - no delegation, one construction:
  constructed: alice, 21 day(s)
  alice, 21 day(s)

2. No arguments - delegates to the two-argument one:
  constructed: (unassigned), 14 day(s)
  ...then the default constructor's own body ran
  (unassigned), 14 day(s)

3. A member only - delegates, and uses the default 14 days:
  constructed: carol, 14 day(s)
  ...then the one-argument constructor's body ran
  carol, 14 day(s)

4. Days only - delegates, and is NOT the same overload as (2):
  constructed: (unassigned), 30 day(s)
  ...then the days-only constructor's body ran
  (unassigned), 30 day(s)

The order surprises nearly everybody. The constructor that does the real work runs first, and the delegating constructor's body runs after it has finished. So Loan carol("carol") prints constructed: carol, 14 day(s) before the one-argument constructor's body gets a turn. If you predicted the opposite, you now know something about delegation that reading the definition alone does not teach: the delegated-to constructor is not a sub-step, it is the construction, and your body is additional work performed on a finished object.

And the two lines in that program that do not compile, both real:

Loan broken(nullptr);
// error: use of deleted function 'Loan::Loan(void*)'

A deleted overload turns "the compiler quietly picks int" into an error. This is the idiom for refusing a nonsense argument while keeping the constructors you want.

Loan implicit = "nobody";
// error: conversion from 'const char [7]' to non-scalar type 'Loan' requested

explicit again, in the copy-initialisation form.

The bug this program contains on purpose

While writing this example, the deleted overload was first written as Loan(bool) = delete; — which looks more sensible than Loan(void*) = delete; and breaks the string overload, refusing Loan carol("carol") with error: use of deleted function 'Loan::Loan(bool)'. The reason is in the file, and in Misconceptions: a const char* converts to bool through a standard conversion, which the compiler always prefers to the user-defined conversion to std::string. Keep the void* version, and keep the story: it is the kind of thing that costs an hour and teaches a rule you never forget.


Example 5 — the class that must not have a destructor yet

No program. An analysis, and it is the bridge to Modules 10 and 11.

class Catalogue {
public:
    Catalogue() : books_(new Book[8]), count_(0), capacity_(8) {}
    bool add(const Book& book);
private:
    Book* books_;
    int count_;
    int capacity_;
};

Question: what does this class need, that it does not have?

Work through it in order:

  1. It needs a destructor. books_ came from new Book[8]. Without ~Catalogue() { delete[] books_; }, those eight books are never released — a leak that happens once per Catalogue. This is the first class in the course that genuinely needs a destructor, and the reason is precise: it owns something the compiler's generated destructor cannot release for it.
  2. It needs a copy constructor. The compiler's generated one copies the pointer. Copy a Catalogue and you get two objects that both believe they own the same array — and both will delete[] it. That is a double free, and it is Module 10's opening disaster.
  3. It needs a copy assignment operator, for the same reason, plus the extra problem that the object on the left already owns an array.
  4. It must not let the default constructor allocate in a way nobody can see. A constructor that calls new on the first line of a class's life is doing exactly the thing Module 3 warns about: an object that is almost valid during construction, and a resource that is unowned if the constructor throws afterwards.

The conclusion of the analysis is the module's honest ending, and Module 11's beginning:

A class that owns a raw resource needs a destructor, a copy constructor and a copy assignment operator — and getting all three right, by hand, is hard enough that C++ added a better answer. That answer is to put the resource inside a member whose own destructor releases it, and then let all of the compiler's generated functions be correct. Module 10 pays the full price of doing it by hand first, on purpose.

What you should take from Example 5 now: the destructor is not automatic cleanup for free. It is a function you write because you own something — and the same sentence that tells you to write a destructor tells you to write a copy constructor.


What to do with this page

  1. Type each program out. Do not copy the files.
  2. Predict every trace in writing before running it. Keep the predictions; the lab asks you to hand one in.
  3. Uncomment every commented-out line in Example 3 and Example 4, one at a time, and record the message. Four of them are in the module's mistakes table.
  4. Then take your own Vehicle or BankAccount class from Module 2, add a lifecycle counter to it, and predict the trace of a five-line main that uses it. That is Lab 3, and doing it here makes the lab thirty minutes instead of three hours.

Next: Misconceptions and mistakes — ten wrong mental models, and the constructor errors you will actually meet. · All modules