Module 3 — Misconceptions and Mistakes¶
Misconceptions are wrong mental models: code that compiles, looks right, and is wrong. They are not fixed by the compiler. Mistakes produce a message, and the message tells you what to do.
Module 3 has more misconceptions than most, because construction and destruction happen silently and everybody builds their first model of them by guesswork.
Part 1: Misconceptions¶
M1. "The constructor body creates the object"¶
By the time the body runs, every member already exists and is already initialised. The order is fixed:
- base class (Module 6),
- members, in declaration order,
- the constructor body.
So this is not "setting up the object", it is additional work on an object that is already set up:
ByAssignment(int amount, int quantity) : amount_(0), quantity_(0) {
amount_ = amount; // the third operation on this member, not the first
}
The practical consequence: anything the body does is too late to initialise. That
covers const members, references, and members whose type has no default constructor —
all three of which simply cannot be given a value in the body, which is why the compiler
rejects them rather than letting you try.
The deeper consequence: a constructor body that can throw leaves you with a partially constructed object and a resource nobody owns. That is why C++ destroyed partly-built objects during stack unwinding long before anyone wrote RAII down, and why Module 10 begins with a constructor that allocates.
M2. "Initialisation and assignment are the same thing"¶
They are different operations, performed at different times, by different functions:
Tracked b = a; // INITIALISATION: the copy constructor. b did not exist yet.
d = a; // ASSIGNMENT: operator=. d already existed and is being changed.
The one-line versions look identical, and that is the trap. They are separated by:
| Initialisation | Assignment | |
|---|---|---|
| Runs | a constructor | operator= |
| Object state before | the object does not exist | the object holds its previous value |
| What a class can refuse | = delete on a constructor |
= delete on operator= |
| Where Module 10 goes wrong | copying a pointer | leaking the object's old resource first |
Why it matters now rather than later: the initialiser list is initialisation. The constructor body is assignment. Once a member is a pointer to something it owns, one of those is a small efficiency question and the other is a double free.
M3. "The initialiser list runs in the order I write it"¶
It runs in declaration order, always. The list's order is not merely ignored — the
compiler warns about it (-Wreorder), because a list you wrote in a different order
usually means you believe something false about your members.
This works only because area_ is declared last. Move int area_; above
int width_; and the same constructor initialises area_ from two members that do not
exist yet. The compiler's three-line warning is the only thing standing between that and
a garbage area — and the course's own builds treat the warning as an error, because
depending on it is not a plan.
The rule is enforced by the language, not by convention, because destruction must be the exact reverse of construction and there is only one order that guarantees it.
M4. "The compiler always gives me a default constructor"¶
The compiler generates a default constructor only if you declare no constructors at all. The moment you write one, it stops:
class Loan {
public:
Loan(int daysAllowed) : daysAllowed_(daysAllowed) {}
private:
int daysAllowed_;
};
Loan loan; // error: no matching function for call to 'Loan::Loan()'
Two refinements worth having ready, because they are the versions that catch people who think they know this rule:
- A constructor with default arguments for every parameter counts as a default
constructor, and therefore also suppresses the generated one:
Loan(int days = 14)meansLoan loan;works andLoan()is not generated. Loan() = default;can still be deleted. If a member has no default constructor, the defaulted constructor is implicitly deleted, and the first error message usually names the member rather than the constructor — which is why it looks mysterious.
The habit that prevents all of this: write = default deliberately, and use member
initialisers (int daysAllowed_ = 14;) so the generated version has something sensible to
do.
M5. "Every class should have a destructor"¶
Most classes written in this course should not have one. Book and Member hold ints
and std::strings; std::string already releases its own memory, and the compiler's
generated destructor destroys each member in reverse order. Adding ~Book() {} changes
nothing.
A class needs a destructor when it owns a resource that must be released — something
the generated destructor cannot release by destroying members. At this point in the
course that means roughly one thing: a raw pointer from new. Later it also means files,
locks and sockets (Module 11).
The dangerous half of this misconception is the reverse: a class that needs a destructor usually needs a copy constructor and a copy assignment operator too. If the generated destructor cannot release the resource, then the generated copy constructor cannot duplicate it either — it copies the pointer. Writing one and not the others is how Module 10's double free happens, and Example 5 of Worked examples is exactly that analysis.
M6. "= default and {} mean the same thing"¶
They produce different kinds of constructor:
Loan() {} // user-provided: not trivial, affects optimisation and what
// the type is allowed to be (e.g. an aggregate)
Loan() = default; // "the generated one": trivial if the members allow it
Two visible differences at this level:
= defaultkeeps the type an aggregate (if nothing else disqualifies it), so brace initialisation continues to work. Writing{}disqualifies it.= defaultkeeps the constructor meaningful: it initialises members using their member initialisers. Writing{}and forgetting one leaves the member indeterminate.
= delete is the other half of the pair and says the opposite: "the operation exists,
and calling it is an error." Prefer it to the old trick of declaring a member function
private and never defining it, because the message describes your intent:
M7. "A temporary dies at the end of the line it appears on"¶
It dies at the end of the full expression — which is the whole statement, semicolon included, not the line inside it:
std::cout << "x: " << compute() << '\n'; // the temporary from compute() outlives
// the earlier parts of this one statement
And there is one extension worth knowing precisely:
Binding to const T& (or T&&) keeps the temporary alive as long as the reference. This
is legal, it is used by real code, and it is not an invitation: the extension follows the
reference, so a reference that outlives its scope leaves you with a dangling reference
and no error. Module 11 discusses where this pattern is genuinely safe.
How to check what you believe: put a print in the destructor and count the lines, as Examples 1 and 3 do. Temporary lifetimes are one of the few areas where the compiler is a better teacher than the documentation.
M8. "Returning an object by value copies it"¶
Under the course's standard (C++17), returning an unnamed value constructs it directly in the caller. No copy, no temporary, no extra destructor:
Tracked makeItem(const std::string& name) {
return Tracked(name); // constructed in the caller's `made`
}
const Tracked made = makeItem("book-three"); // ONE construction
This guarantee — mandatory copy elision — is why the course pins -std=c++17 in every
command. Under C++14 the same program is allowed to create and destroy a temporary, so
a lifecycle trace could differ between compilers. A prediction exercise should not depend
on which compiler you use.
Do not over-read this into "returning by value is always free":
- A named local returned by value (
Tracked t(...); return t;) is elided by every mainstream compiler but is not guaranteed by the standard — the move constructor must exist and be callable, or the program is ill-formed. - A by-value parameter is always a copy. Elision applies to return values, not arguments.
M9. "Deleting an overload just removes that call"¶
Deleting an overload changes which overload the compiler selects for every call whose arguments could go either way. This is a real bug from this module's own programs:
Loan(bool) = delete; // looks sensible: refuse Loan(true)
Loan loan("alice"); // ✗ error: use of deleted function 'Loan::Loan(bool)'
Why: const char* → bool is a standard conversion, and const char* →
std::string is a user-defined conversion. The standard says a standard conversion
wins, so the compiler preferred the deleted bool overload for a "alice" argument.
The fix is to delete the type that captures the arguments you mean to refuse:
Loan(void*) = delete; // ✓ refuses `Loan broken(nullptr);`
Loan loan("alice"); // ✓ reaches the std::string overload
The general rule: when overload resolution surprises you, list the candidates and rank
their conversions — exact match, then promotion, then standard conversion, then
user-defined conversion. The compiler is being consistent, not obtuse, and it has a
-fdiagnostics-all-candidates flag (mentioned in the message itself) that prints the
ranking when you cannot see it.
M10. "A delegating constructor's body runs first"¶
The opposite. The constructor you delegate to runs completely — members initialised, its body finished — and only then does the delegating constructor's body run:
constructed: carol, 14 day(s) ← the delegated-to constructor, finished
...then the one-argument constructor's body ran
Two consequences: your body is working on a fully constructed object, and it cannot "adjust" initialisation — it can only do more work afterwards. If you need to change something the delegated-to constructor decided, that is a design problem, not a sequencing problem.
Also: a delegating constructor's initialiser list may contain only the delegation. It cannot initialise members as well, because they are already initialised.
Part 2: Mistakes, with their real messages¶
Every message below was produced by compiling the code shown, with the course's own flags. The text is real.
D1 — assigning to a const member in the constructor body¶
error: uninitialized const member in 'const int' [-fpermissive]
note: 'const int A::v_' should be initialized
error: assignment of read-only member 'A::v_'
Two errors for one mistake, and between them they say the whole thing: the member was
never initialised, and you tried to write to something read-only. Fix: A(int v) : v_(v) {}.
D2 — a reference member¶
error: uninitialized reference member in 'int&' [-fpermissive]
note: 'int& B::r_' should be initialized
A reference cannot be rebound after creation, so assigning one in the body is meaningless
— and the compiler says so by naming the initialisation you skipped. Fix:
B(int& r) : r_(r) {}.
D3 — a member type with no default constructor¶
class C {
public:
C(int v) { s_ = Slot(v); }
private:
Slot s_; // Slot has only `explicit Slot(int)`
};
error: no matching function for call to 'Slot::Slot()'
note: candidate: 'Slot::Slot(int)' — candidate expects 1 argument, 0 provided
error: use of deleted function 'Slot& Slot::operator=(const Slot&)'
The first error is the real one: the body's assignment requires s_ to have been
constructed already, and there is no default constructor to construct it with. The second
is a consequence. Fix: C(int v) : s_(v) {}.
This is the error to recognise in the wild, because the message names the member's
constructor rather than yours, and beginners read it as "the compiler is confused about
Slot".
D4 — the initialiser list in the wrong order¶
warning: 'Rectangle::height_' will be initialized after [-Wreorder]
warning: 'int Rectangle::width_' [-Wreorder]
warning: when initialized here [-Wreorder]
Not an error under plain -Wall -Wextra; an error under the course's build, which adds
-Werror. Read the three lines as a sentence: "height_ will be initialised after
width_ — when initialised here". The fix is to write the list in declaration order,
even when the values make a different order more natural. It only looks like a
diagnostic. This warning is one of the last defences against initialising a member from
a member that does not exist yet.
D5 — the default constructor that was never generated¶
Wait — that reads like a mistake in your function, and it is not. Session declared
another constructor, so no default constructor exists. Fix: write one, delegate one
(Session() : Session("guest") {}), or stop default-constructing.
D6 — a conversion explicit refused¶
The array length in the message is the string literal's length including the terminating
null ("bob" is const char[4]), which is a useful confirmation that the compiler is
talking about your literal and not about something else. Fix: Session s("bob");, or drop
explicit if the conversion really is intended.
D7 — a deleted function¶
The note points at your = delete line, which is the compiler agreeing that this is
deliberate. If a call is supposed to work and lands here, see M9: an overload you did
not mean to select is being selected.
D8 — deleting twice, or not at all (preview)¶
Not from this module's programs, because they own nothing yet — but the shape of the message is worth meeting before Module 10:
That is a run-time crash with no source location at all, produced by a class that owns a raw resource and relies on the compiler's generated copy constructor. It is the reason Module 10 exists, and it is the single most valuable disaster in the course to cause deliberately once, in a program you wrote yourself.
The two-minute check for any class you write¶
- Does it own anything? If yes, this class needs a destructor — and a copy constructor, and a copy assignment operator. Write all three or none (Module 10).
- Is every member in the initialiser list? Not "assigned somewhere" — initialised in the list. If not, can you say why not?
- Is the list in declaration order? If not, the compiler is warning you and you have probably misread which member depends on which.
- Does any single-argument constructor lack
explicit? Does anything need the implicit conversion? If not, mark it. - Is every path into this class valid? Walk each constructor: can it produce an object that violates the invariant? If it can, that is Module 4's subject, arriving early.
- Does any constructor allocate or acquire? Then ask what happens if the next line throws. Module 10 makes that question urgent; asking it now costs nothing.
Next: Debugging — predicting a trace you cannot see, and the escalation ladder. · All modules