Skip to content

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:

error: no match for 'operator!=' (operand types are 'Book' and 'Book')

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:

for (const Book& a : sample) {
    assert(!(a < a));
}

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.

error: invalid use of member 'Book::id_' in static member function

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:

error: no match for 'operator<' (operand types are 'Book' and 'Book')

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:

std::ostream& operator<<(std::ostream& out, const Book& book) const;

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.

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

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

  1. Left operand: what is it, and is it my type? If not, the operator is free.
  2. Consistency: do ==, < and printing agree about what makes two values equal? Write the one sentence that defines identity.
  3. Strictness: is < false for equal values, and transitive? Test it with a duplicate in the sample.
  4. Derivation: how many comparisons did I write by hand? If the answer is six, two of them are inconsistent.
  5. Necessity: does the operator mean what a reader expects — or would a named function be clearer?
  6. The four to avoid: did I overload &&, ||, , or &? If yes, delete it.
  7. static state: is every static data 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