Skip to content

Module 10 — Misconceptions and Mistakes

Two different problems, fixed two different ways.

A misconception is a wrong mental model. Only evidence removes it, so each one below is paired with the smallest program that contradicts it. A mistake is a concrete error; information fixes it, so each one below comes with the message the compiler actually printed and the corrected code.

Every diagnostic on this page was produced by compiling the snippets shown, with the course's flags, on the toolchain described in Setting up. Quote them to yourself as you read: the wording of a message is how you will recognise it in six months.


Part 1 — Misconceptions

"The compiler's copy is always fine."

Why this feels true. It has been true in every module so far. Every class you have written has been copied, returned, passed by value and stored in containers, and the compiler's member-wise copy was right each time — because every member was a value or a standard type.

Where it breaks. Copy a class that owns a raw pointer, and predict the two counters before you look:

RawBuffer original(4);
RawBuffer copy = original;
std::cout << copy.sharesStorageWith(original) << '\n';   // predict this line

What you get. 1 — one block, two owners. examples/module-10/01-shallow-copy-alias prints the whole story, ending with second frees BLOCKED 1: the guard in that instrumented destructor is all that separates the demonstration from a crash.

The correct model. Member-wise copy is right for values and wrong for owned resources. std::string is fine to copy because std::string's own copy constructor allocates and copies the characters. A bare int* is not, because there is no such constructor — the pointer has no idea it owns anything. The property that makes a class safe to copy by default is that every member manages its own resource.

The re-test. Later in this page: a class with a std::unique_ptr member. Does the compiler still generate a copy? What happens when something copies it?


"std::move moves something."

Why this feels true. It is called move. It reads like an instruction, and it appears at the call site of something that then does in fact move.

Where it breaks. The name describes the effect on overload resolution, not the action. std::move is a cast:

template <typename T>
std::remove_reference_t<T>&& move(T&& value) noexcept;   // that is the whole function

So a move only happens if the cast makes a move overload selectable. Two cases where it does not:

consume(d);                      // plain lvalue:  copies 1  moves 0
consume(std::move(constMember)); // const:         copies 1  moves 0

The correct model. std::move converts an expression into an rvalue, which allows T&& overloads to be chosen. If no move overload is viable — because none exists, or because the argument is const — the copy is chosen, silently and correctly.

The re-test. Add a const qualifier to a member of examples/module-10/04-move-semantics's Tracked and count again. The move becomes a copy, and nothing warns.


"After a move, the source object is empty and unusable."

Why this feels true. "Moved from" strongly suggests emptied, and in practice the standard library's own types do leave an empty object behind — a moved-from std::string usually has size 0, and examples/module-10/04-move-semantics shows exactly that.

Where it breaks. The standard guarantees only that a moved-from object is valid but unspecified: you may destroy it, and you may assign to it. Reading its value is not guaranteed to give anything in particular. On one implementation it is empty; on another it may hold the original value, or something else entirely.

Tracked source("source", {1, 2, 3, 4, 5});
Tracked taken = std::move(source);
// source is valid: safe to destroy, safe to reassign.
// source.size() is UNSPECIFIED. Do not branch on it.

The correct model. Treat a moved-from object like a variable that has been assigned an unknown value: fine to reuse, never safe to inspect. If your own move operations leave it empty, that is a good choice — write it in the header comment, and still do not read it.

The re-test. Which single guarantee about a moved-from object may you rely on in a container's element type? (That it can be destroyed and assigned to — nothing else.)


"noexcept is documentation."

Why this feels true. It reads like a comment: a promise about behaviour that the compiler mostly does not check, and it changes nothing at the call site.

Where it breaks. A std::vector will not use a move constructor during reallocation unless it is noexcept, because it must keep the strong guarantee. -Wall -Wextra -Werror does not mention it, and neither does the class:

Item<true>,  noexcept move:   constructions 15, copies 0, moves 7
Item<false>, throwing move:   constructions 15, copies 7, moves 0

The correct model. noexcept on move operations is a requirement of the standard library's optimisation, not a description of your class. Without it you get copies, more allocations, and no diagnostic anywhere.

The re-test. Which two functions must be noexcept, and what breaks if they are not? (Both move operations; containers copy instead of moving.)


"The Rule of Three means always write all three."

Why this feels true. It is usually taught as an obligation: destructor, copy constructor, copy assignment — write them.

Where it breaks. The Rule of Three tells you when those three come as a set. It is not a target. The best way to obey it is to make it unnecessary:

class Report {
    std::string title_;
    std::vector<int> figures_;
    // no destructor, no copy constructor, no copy assignment, no moves: correct anyway
};

The correct model. Rule of Three → Rule of Five (modern C++) → Rule of Zero (the goal). Roughly: if you must take control of one resource, take control of all five operations; better, own nothing directly and declare none of them.

The re-test. In examples/module-10/06-rule-of-zero-and-elision, which special members does Report declare, and what does the compiler generate for it?


"If I define a destructor, nothing else changes."

Why this feels true. A destructor releases a resource. Declaring one is a private matter for that class.

Where it breaks. Declaring a destructor suppresses the compiler's move operations, so moves quietly become copies. Declaring a move operation deletes the copy operations. This is a change to the class's interface, and the compiler enforces it at every call site:

error: use of deleted function 'constexpr MoveOnly::MoveOnly(const MoveOnly&)'
note: 'constexpr MoveOnly::MoveOnly(const MoveOnly&)' is implicitly declared as deleted
      because 'MoveOnly' declares a move constructor or move assignment operator

The correct model. The five special member functions are one interdependent group. Decide for the whole group — writing = default or = delete for every one of them — and the group behaves predictably. Section 3 of Concepts has the full table.

The re-test. A class declares ~Widget(); and nothing else. Which operations does it now have, and which did it lose?


"Self-assignment never happens in real code."

Why this feels true. Nobody writes a = a. It looks like a synthetic example invented to justify a defensive line.

Where it breaks. Self-assignment arrives through an alias, not through a literal:

void assignMax(std::vector<Buffer>& buffers) {
    for (std::size_t i = 0; i < buffers.size(); ++i) {
        if (buffers[i].size() < buffers[0].size()) {
            buffers[i] = buffers[0];      // when i == 0, this is buffers[0] = buffers[0]
        }
    }
}

The correct model. Any assignment where the source and target can be the same object must be safe, because you cannot prove they are different at the point you write the operator. The fix is one line (if (this == &other) { return *this; }), or copy-and-swap, which removes the question. The same worry applies to the move assignment operator — see the mistake below, where the failure is silent.

The re-test. Which two places in an owning class can destroy the object's own resource through an alias? (Copy assignment and move assignment.)


"A destructor is needed whenever a class has a pointer."

Why this feels true. Pointers look like resources. Every owning pointer in this course so far has had a destructor.

Where it breaks. A pointer that does not own anything needs no destructor, and giving it one is a bug:

class Timetable {                 // Module 9
    Header header_;               // a value: owned
    SessionList sessions_;        // a value: owned
    Course* owner_ = nullptr;     // borrowed. NOT owned. No destructor needed.
};

The correct model. The destructor releases what the class owns. Whether it owns is a design decision, and the type should say so: a value member owns, a std::unique_ptr owns, a reference does not, and a raw pointer does not unless you have written that meaning down and are the only one who does.

The re-test. In examples/module-09/03-value-pointer-or-reference-member, which of the four forms needs a destructor, and which must not have one?


"A shallow copy leaks memory, and a deep copy fixes it."

Why this feels true. The symptom of a missing copy constructor is memory trouble, and a deep copy makes the memory trouble go away.

Where it breaks. A shallow copy does not leak; it double-frees. The leak is the opposite failure — a constructor that allocates and never releases, or an assignment that overwrites a pointer without deleting what it held. Both are real, and they have different fixes:

Symptom Cause
Crash at destruction; on glibc, a message naming a double free Two objects own one block, and both delete it
Memory grows as the program runs Something allocated and nothing released it

The correct model. Count. A class is correct when every object's construction count and destruction count match, and when allocations == frees across the program. The counters in examples/module-10/02-rule-of-three are printed for exactly this reason.

The re-test. An assignment operator that does data_ = new int[n]; without releasing data_ first: which of the two symptoms do you get, and how would you detect it?


Part 2 — Common programming mistakes

Symptom Cause Fix
Crash inside the allocator at the end of a scope; a second free of the same address Shallow copy of an owning pointer Deep copy, = delete, or Rule of Zero
Crash or corruption on a = a Assignment destroyed its own source Guard with this == &other, or copy-and-swap
Data appears to change "by itself" after a copy Two objects share one block Deep copy
error: use of deleted function 'X::X(const X&)' The copy is deleted and something copies Take a reference, move instead, or make the type copyable
error: use of deleted function 'constexpr X::X(const X&)' with note: … declares a move constructor Declaring a move operation deleted the copy Declare the ones you want, or = default the copies
error: passing 'const X' as 'this' argument discards qualifiers Assigning to a const object Remove the const, or assign to a non-const object
error: no match for 'operator=' (operand types are 'X' and 'void') An assignment operator returning void Return X&
Silent copies where moves were expected A const member, missing move operations, or a move that is not noexcept Remove the const, declare the moves, add noexcept
A moved-from object "loses" its value unexpectedly Move assignment with no self-move guard, reached through an alias Guard on this == &other in the move operations too
A program that works in a debug build and fails in release Undefined behaviour, which is not required to fail Find it with a sanitizer or with counters, not by reading

Mistake 1 — the copy that could not exist [L3 Modify]

Reproduce it.

class NoCopy {
public:
    NoCopy(const NoCopy&) = delete;
    NoCopy& operator=(const NoCopy&) = delete;
};

int main() {
    NoCopy first;
    NoCopy second(first);
}

What you see.

error: use of deleted function 'NoCopy::NoCopy(const NoCopy&)'
note: declared here

Why. A deleted function still participates in overload resolution: the copy constructor was chosen, and then rejected. That is why the message names a function you cannot see rather than saying "no matching function".

Fix it. Pass by reference, move the object, or hold it through a smart pointer:

void use(const NoCopy& value);   // no copy

Prevent it. When you write = delete, add a one-line comment saying what callers should do instead. A class that refuses copying without offering an alternative is a design problem disguised as a compile error.


Mistake 2 — the copy that vanished when you added a move [L3 Modify]

Reproduce it.

class MoveOnly {
public:
    MoveOnly() = default;
    MoveOnly(MoveOnly&&) = default;                 // declaring this deletes the copy
    MoveOnly& operator=(MoveOnly&&) = default;
};

int main() {
    MoveOnly first;
    MoveOnly second(first);
}

What you see.

error: use of deleted function 'constexpr MoveOnly::MoveOnly(const MoveOnly&)'
note: 'constexpr MoveOnly::MoveOnly(const MoveOnly&)' is implicitly declared as deleted
      because 'MoveOnly' declares a move constructor or move assignment operator

Why. The compiler infers intent from what you declared. Declaring a move operation says "this type is moved, not copied", and it acts on that inference by deleting the copies. The note: states the rule more clearly than most documentation does.

Fix it. Either accept being move-only — many types are, correctly — or declare the copies you want:

MoveOnly(const MoveOnly&) = default;      // or write it out, if members need work
MoveOnly& operator=(const MoveOnly&) = default;

Prevent it. Decide for the whole group of five at once. If you write one, ask what it implies about the other four, and write (or = default, or = delete) every one of them.


Mistake 3 — assigning to a const object [L2 Apply]

Reproduce it.

class Counter {
public:
    Counter& operator=(const Counter&) = default;
private:
    int value_ = 0;
};

int main() {
    const Counter counter;
    Counter other;
    counter = other;
}

What you see.

error: passing 'const Counter' as 'this' argument discards qualifiers [-fpermissive]
note:   in call to 'constexpr Counter& Counter::operator=(const Counter&)'

Why. Assignment modifies *this, so it can never be called on a const object. The compiler is not complaining about your operator; it is complaining that you asked a const object to change.

Fix it. Either the object should not be const, or the change belongs somewhere else — often in a member marked mutable, or in a freshly constructed object.

Prevent it. The same message appears in Module 4's material, and for the same reason: const propagates through member function calls. counter = value on a const & parameter is a design question, not a syntax problem.


Mistake 4 — an assignment operator that returns void [L2 Apply]

Reproduce it.

class ChainedVoid {
public:
    void operator=(const ChainedVoid&) {}     // returns void
};

int main() {
    ChainedVoid x, y, z;
    x = y = z;
}

What you see.

error: no match for 'operator=' (operand types are 'ChainedVoid' and 'void')
note: candidate: 'void ChainedVoid::operator=(const ChainedVoid&)'
note:   no known conversion for argument 1 from 'void' to 'const ChainedVoid&'

Why. y = z evaluated to void, so x = (void) has no viable overload. Assignment must return a reference to the assigned object for chaining to work — the same as the built-in types.

Fix it.

ChainedVoid& operator=(const ChainedVoid& other) {
    if (this == &other) { return *this; }     // and the guard while you are there
    return *this;
}

Prevent it. Write the signature from the canonical form and fill in the body. T& operator= and T& operator= for the move version: the return type is not a stylistic choice.


Mistake 5 — the self-move that silently emptied the object [L4 Analyze]

Reproduce it. A move assignment with no self-check, reached through an alias:

class Tracked {
public:
    Tracked& operator=(Tracked&& other) noexcept {
        name_ = std::move(other.name_);        // if other is *this, we move a string into itself
        return *this;
    }
private:
    std::string name_;
};

Tracked a("a");
Tracked& alias = a;
a = std::move(alias);                          // legal, compiles, no warning

What you see.

aliased self-assign: copies 0 moves 1 name now ""
pointer self-assign: copies 0 moves 1 name now ""

The object destroyed its own contents, and the compiler said nothing. Note that the program did not crash: the object is still valid and destructible, so the bug is silent — it shows up later as an empty string where a value was expected.

Why. name_ = std::move(other.name_) where other is *this is a self-move-assignment of a std::string, which is permitted to leave the string empty. The standard allows self-move-assignment to do essentially anything.

Fix it.

Tracked& operator=(Tracked&& other) noexcept {
    if (this == &other) { return *this; }      // one line, and the whole failure mode goes away
    …
}

Prevent it. Put the guard in the move assignment operator as reflexively as in the copy assignment operator. Both can be reached through an alias; only one of them warns you, and that one is neither.


Mistake 6 — the move that was never allowed to happen [L4 Analyze]

Reproduce it. return std::move(local); from a function returning by value:

T pessimal() {
    T local("pessimal");
    return std::move(local);          // buys a move where nothing was needed
}

What you see.

warning: moving a local object in a return statement prevents copy elision
         [-Wpessimizing-move]
note: remove 'std::move' call

and the counters agree: return std::move(local) produces copies 0 moves 1, while return local; produces copies 0 moves 0. The version that looks more optimised is the one that does more work.

Why. Returning a named local is a case the compiler is permitted to elide — construct the object directly in the caller's storage — and most compilers do. Writing std::move makes the return expression an rvalue reference to a local, which forbids the elision. You asked for a move and paid for it.

Fix it.

T correct() {
    T local("correct");
    return local;                     // NRVO, or a move — both cheap, neither worse
}

Prevent it. Treat -Wpessimizing-move as an error: the course's build already promotes it with -Werror. Never write std::move on a local being returned. (In other places — passing an object to a parameter you know is done with it — std::move is exactly right.)


Next: Debugging — how to find a memory bug that no compiler will point at, including what to do when a sanitizer is not available.