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
constmember cannot be assigned in the body at all. Try it and the compiler saysuninitialized const member in 'const int'andassignment of read-only member. TheTicket::code_member in this program isconst, 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.
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:
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:
- It needs a destructor.
books_came fromnew Book[8]. Without~Catalogue() { delete[] books_; }, those eight books are never released — a leak that happens once perCatalogue. 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. - It needs a copy constructor. The compiler's generated one copies the pointer.
Copy a
Catalogueand you get two objects that both believe they own the same array — and both willdelete[]it. That is a double free, and it is Module 10's opening disaster. - It needs a copy assignment operator, for the same reason, plus the extra problem that the object on the left already owns an array.
- It must not let the default constructor allocate in a way nobody can see. A
constructor that calls
newon 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¶
- Type each program out. Do not copy the files.
- Predict every trace in writing before running it. Keep the predictions; the lab asks you to hand one in.
- 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.
- Then take your own
VehicleorBankAccountclass from Module 2, add a lifecycle counter to it, and predict the trace of a five-linemainthat 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