Module 5 — Misconceptions and Mistakes¶
Two different problems, fixed two different ways.
A misconception is a wrong mental model, and only evidence removes it — which is why each one below ends with a program that contradicts it.
A mistake is a concrete error, and information fixes it. The compiler messages in Part 2 are the real ones from this course's toolchain, so you can match them against yours.
Part 1: Misconceptions¶
M1. "Book b = a; uses operator="¶
Why this feels true. The = sign is right there, and the line looks like
assignment written a slightly odd way.
Where it breaks.
struct Track {
int id;
Track(int i) : id(i) {}
Track(const Track& o) : id(o.id) { std::cout << "copy\n"; }
Track& operator=(const Track& o) { id = o.id; std::cout << "assign\n"; return *this; }
};
Track a(1);
Track b = a; // prints: copy
Track c(2);
c = a; // prints: assign
The correct model. The = in a declaration is initialisation syntax. It
means "construct a new object, using this value" — here, the copy constructor. The
assignment operator only ever runs on an object that already exists. The question
that always answers it: did the object already exist before this line? If not,
it is construction, whatever the syntax looks like.
Why it matters beyond curiosity: a type that frees a resource in one function and copies it in the other will do the wrong thing on exactly one of these two lines. Module 10 is built on this distinction.
The re-test. Six lines of code, four of which contain =. Only one of them
calls the assignment operator. Which?
M2. "If I define operator==, I get != for free"¶
Why this feels true. Comparison feels like one idea, and some languages derive
the pair automatically — C++20 does, in fact, when you write operator<=>.
Where it breaks. This course uses C++17, and in C++17 they are two separate functions:
The correct model. == is a function, != is a different function with a
different name. Write == and < by hand and derive the other four from them, so
they cannot contradict each other:
bool operator!=(const Book& other) const { return !(*this == other); }
bool operator>(const Book& other) const { return other < *this; }
bool operator<=(const Book& other) const { return !(other < *this); }
bool operator>=(const Book& other) const { return !(*this < other); }
The re-test. A type defines == and < only, and four derived comparisons.
Which of the six would a reviewer ask you to prove consistent, and how would you
prove it in three lines?
M3. "operator<< can be a member of my class"¶
Why this feels true. Most operators can be members, and it is natural to think of printing as something the book does.
Where it breaks.
error: 'std::ostream& Book::operator<<(std::ostream&, const Book&) const'
must have exactly one argument
The correct model. a << b calls a.operator<<(b) when it is a member, so
*this is the left operand. To write std::cout << book, the left operand must be
the stream — a type you do not own and cannot add members to. So << is a free
function taking std::ostream& first, and returning it so that chaining works.
The re-test. std::cout << a << b; is two calls. Write them out as nested
function calls, and say what the return type of the inner one must be.
M4. "A sort that compiles and runs is correct"¶
Why this feels true. It produced output, and the output is in an order.
Where it breaks. std::sort requires a strict weak ordering. A comparator
that returns true for equal elements violates it, and the standard calls the
result undefined behaviour: not "a different order", but "anything at all".
bool operator<(const Book& other) const {
return title_ <= other.title_; // wrong: `a < a` is now true
}
The correct model. < must be irreflexive (a < a is false), asymmetric, and
transitive. Test it directly rather than through a sort:
The re-test. Given a comparator and five elements, including two that are
equal, what will std::sort produce? The honest answer is "something, and it is
not guaranteed to be the same thing twice" — and naming the violated property is
what earns the mark.
M5. "static means one per object"¶
Why this feels true. The single place static appeared before this module was
a local variable inside a function, where the point was that it survives between
calls — so it reads as "kept", i.e. per where it is written.
Where it breaks. examples/module-05/01-static-members/main.cpp prints
live now: 3 and then, after all three objects are gone, live: 0 — one counter
decremented by three destructors.
The correct model. A static data member belongs to the class: one
storage location, shared by every object, existing even when no object does. A
static local variable is a different meaning of the same keyword: one per
function, living until exit. A static free function is a third: internal
linkage. Learn them as three separate facts.
The re-test. Two Ticket objects print liveCount(). If you change the venue
through one object, what does the other report? Why is that not "leaking"?
M6. "A static member function can use the object's members"¶
Why this feels true. It is written inside the class, surrounded by the members, so it looks like it has access.
Where it breaks.
The correct model. A static member function is called on the class
(Book::count()), so there is no object and therefore no this. It cannot read a
non-static member, and it cannot be const, because const qualifies *this —
and there is no *this to qualify. If it needs an object's data, take one as a
parameter.
The re-test. Why is a static function the right place for a fromIsbn()
factory, and why can that factory not be const?
M7. "A static data member is defined where it is declared"¶
Why this feels true. The declaration inside the class looks complete —
static int live_; is a whole statement, and the compiler is happy with it.
Where it breaks. The program compiles and then fails to link, with the linker
naming the member (undefined reference to 'Ticket::live_'). A link error, not a
compile error: the compiler only ever saw a declaration, and only the linker
notices that no definition exists.
The correct model. A static data member is declared in the class and
defined once outside it — int Ticket::live_ = 0; — unless it is inline
(C++17) or a constexpr member of integral type initialised in the class. This is
the One Definition Rule, and Module 16 makes it concrete across many files.
The re-test. Give two correct fixes for the link error, and one sentence on when each is preferable.
M8. "Overloading an operator lets me change what it means"¶
Why this feels true. "Overload" sounds like "redefine", and the syntax makes
a + b look like something you control.
Where it breaks.
error: 'Book& operator=(Book&, const Book&)' must be a non-static member function
error: 'std::ostream& Book::operator<<(std::ostream&, const Book&) const'
must have exactly one argument
The correct model. At least one operand must be a class or enumeration type,
so 1 + 2 can never mean anything but 3. ::, ., .*, ?:, sizeof,
typeid, alignof and the named casts cannot be overloaded at all — the first
three are how names are resolved, and redefining them would make programs
unparseable. And &&, ||, , and & can be overloaded, and should not be,
because each carries a guarantee you would destroy (M4 of this module's concepts
page measures one of them).
The re-test. Name three operators that cannot be overloaded, and give the reason in terms of name resolution rather than "the standard says so".
Part 2: Mistakes¶
The table first, then the ones worth a worked fix.
| Symptom | Cause | Fix |
|---|---|---|
error: no match for 'operator<' (operand types are 'Book' and 'Book'), inside a long template expansion |
the type has no operator< |
define one, and make it a strict weak ordering |
std::sort runs and the order is wrong, or the program crashes |
the comparator is not a strict weak ordering | ensure < is false for equal elements; compare the field that defines the order |
error: 'operator<<' must take exactly one argument |
written as a member | make it a free function taking std::ostream& and const T& |
error: 'operator=' must be a non-static member function |
written as a free function | =, [], (), -> are members; there is no alternative |
error: no match for 'operator!=' |
== exists, != was never written |
derive != from == |
undefined reference to 'Ticket::live_' |
static member declared, never defined | add int Ticket::live_ = 0; in the .cpp, or use inline static (C++17) |
error: invalid use of member 'Book::id_' in static member function |
this does not exist there |
take the object as a parameter, or make the function non-static |
error: passing 'const Book' as 'this' argument discards qualifiers on a < b |
operator< is not const |
add const; a sort compares const elements |
| the counter keeps growing although objects are destroyed | --live_ missing from the destructor |
instrument both ends — the same habit as the Module 3 lifecycle trace |
| two books compare equal but print differently | == uses the identifier, printing shows the title |
decide what identity means and use it in ==, < and printing alike |
D1 — the sort that does not compile, and the useful line in the message¶
Reproduce it. Define Book with private id_ and title_, and no comparison
operators. Call std::sort(books.begin(), books.end());.
What you see. Twelve lines of no matching function notes, ending in:
Why. std::sort is a template. It compiles successfully for any type with
<, and fails for yours inside the library header that performs the comparison —
which is why the useful line is at the bottom, and why the ones above it are
between library internals.
Fix it. Define bool operator<(const Book&) const, comparing the identifier.
Prevent it. Read template error messages from the bottom. The last error: is
usually the one about your code; the notes above it are the compiler explaining how
it got there.
D2 — operator<< written as a member¶
Reproduce it. Inside the class:
What you see.
error: 'std::ostream& Book::operator<<(std::ostream&, const Book&) const'
must have exactly one argument
Why. A member function already has *this as an operand, so this declaration
describes an operator with three operands. There is no such <<.
Fix it. Move it outside the class, and declare the friend only if the members are private and no accessor will do:
std::ostream& operator<<(std::ostream& out, const Book& book) {
return out << '#' << book.id() << ' ' << book.title();
}
Prevent it. One habit: before writing an operator, ask "what is the left operand?" If the answer is not your type, it cannot be a member.
D3 — the static member that never got defined¶
Reproduce it. Declare static int live_; in the class, use
Ticket::liveCount(), and do not write the definition outside the class.
What you see. Compilation succeeds. The link fails, naming the member:
undefined reference to 'Ticket::live_'.
Why. The class body declares the member's type; it does not reserve storage.
One definition, in exactly one translation unit, does that. A static member is
the only place in C++ where you write a variable's name with its class prefix
outside the class in this way, which is why it is forgotten.
Fix it. Two correct fixes:
// (a) the C++17 way: define it in the class
inline static int live_ = 0;
// (b) the classic way: declare in the class, define in the .cpp
// header: static int live_;
// source: int Ticket::live_ = 0;
Prevent it. Prefer (a). It removes a line that can be forgotten, keeps the
initialiser visible where the declaration is read, and is what the course's own
examples use for venue_.
D4 — a non-const comparator¶
Reproduce it. Write bool operator<(const Book& other) { ... } — no trailing
const — and sort a const std::vector<Book>&.
What you see.
Why. std::sort takes iterators and compares elements by const reference at
some points; calling a non-const member on a const object is exactly what
Module 4's const-correctness forbids.
Fix it. Add const. It costs nothing and prevents a whole class of failure.
Prevent it. Write comparison operators with the full pattern every time:
bool operator<(const Book& other) const. Then run Module 4's attack test on the
type: a const Book should be printable, comparable and copyable.
The two-minute check for any type you give operators to¶
- Left operand: what is it, and is it my type? If not, the operator is free.
- Consistency: do
==,<and printing agree about what makes two values equal? Write the one sentence that defines identity. - Strictness: is
<false for equal values, and transitive? Test it with a duplicate in the sample. - Derivation: how many comparisons did I write by hand? If the answer is six, two of them are inconsistent.
- Necessity: does the operator mean what a reader expects — or would a named function be clearer?
- The four to avoid: did I overload
&&,||,,or&? If yes, delete it. staticstate: is everystaticdata member defined somewhere, and can I say who is allowed to change it?
Next: Debugging — the technique this module adds: testing a comparator as a relation rather than through a sort. · All modules