Skip to content

Module 11 — Misconceptions and Mistakes

Two different problems, fixed two different ways.

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

Every diagnostic quoted here was produced by compiling the snippets shown, with the course's flags, on the toolchain described in Setting up.


Part 1 — Misconceptions

"RAII is about memory."

Why this feels true. Every example in every tutorial is memory, because C++ is where memory management is taught. The acronym is even usually demonstrated with new and delete.

Where it breaks. Rewrite Example 1's guard for a file, a lock, or a device, and the memory part disappears while the property stays:

class FileGuard {
public:
    explicit FileGuard(const std::string& path) : file_(std::fopen(path.c_str(), "a")) { … }
    ~FileGuard() noexcept { if (file_ != nullptr) { std::fclose(file_); } }
    FileGuard(const FileGuard&) = delete;
    FileGuard& operator=(const FileGuard&) = delete;
    void write(const std::string& line);
private:
    std::FILE* file_;
};

The correct model. RAII is a statement about when release happens — at scope exit, on every path, including the propagating-exception path. That is a fact about control flow, not about memory. Any resource with a release step qualifies: memory, files, sockets, locks, connections, device handles.

The re-test. Which parts of Example 6's device handling would change if the resource were a mutex instead of a device? (The release function. Nothing else — which is the point.)


"Smart pointers are just syntax for pointers."

Why this feels true. They are written almost the same way, dereferenced the same way, and used in the same places. The compiler generates the same machine code for unique_ptr as for a raw pointer.

Where it breaks. The difference is what the type forbids:

std::is_copy_constructible<std::unique_ptr<Shape>> -> false
std::is_move_constructible<std::unique_ptr<Shape>> -> true
std::unique_ptr<Shape> owner = std::make_unique<Circle>(2.0);
std::unique_ptr<Shape> copy = owner;        // does not compile
error: use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr(
       const std::unique_ptr<_Tp, _Dp>&) [with _Tp = Shape; _Dp = std::default_delete<Shape>]'
note: declared here

The correct model. A raw pointer is an address with no ownership information. A smart pointer is an ownership claim that the compiler enforces: one owner for unique_ptr, a counted set of owners for shared_ptr, a non-owner for weak_ptr. The zero-cost claim is about run time; the benefit is at compile time.

The re-test. Which two of these four signatures tell a caller that ownership moves? T*, T&, unique_ptr<T>, const T&. (Only the third, and that is the whole reason to prefer it over a T* parameter that also happens to delete.)


"shared_ptr is the modern *, so use it everywhere."

Why this feels true. It sounds like the safe, modern, garbage-collected choice, and it removes Module 10's double free, which is the bug you have just spent a week on.

Where it breaks. It is two pointers wide instead of one, it counts atomically on every copy, and — the part that costs real time — it invites the cycle in Example 4. A shared_ptr you did not need is a slower program with a new failure mode.

The correct model. shared_ptr answers exactly one question: can no single owner be named? If one owner can be named, unique_ptr expresses that with no cost and no cycle risk. If nothing owns it, a reference or a non-owning pointer says so. "Shared" is a claim about the design, and it should be a true one.

The re-test. In Lab 11 you must write down one place where you rejected shared_ptr. If you cannot find one, look again at the objects whose lifetime is simply the function's.


"shared_ptr prevents leaks."

Why this feels true. It prevents the specific leak from Module 10 — the object always dies when the last owner dies. That is real, and it is why the type exists.

Where it breaks. Two owners that own each other never let go:

Part B - the cycle: a leak that no pointer can be blamed for
      [graph] first use_count 2, second use_count 3, third use_count 2
  alive now 3   <- three nodes have leaked

The correct model. shared_ptr prevents premature destruction and double destruction. It does not prevent permanent retention, and a cycle is exactly that. Owning relationships must be acyclic; where the relationships genuinely form a loop, one link must be a weak_ptr.

The re-test. Why does a leak checker report a cycle as still reachable rather than definitely lost? (Nothing is unreachable — the objects are reachable from each other. There is no invalid access and no lost pointer, so there is no error to report; only a design mistake.)


"The reference count protects the object."

Why this feels true. Every description of shared_ptr mentions that the count is thread-safe, and "thread-safe" is an easy word to over-read.

Where it breaks. Two threads may safely copy and destroy shared_ptrs to the same object, and still corrupt the object itself, because the count and the object are different things in different places:

shared_ptr<Config> a = ...;        // count lives in the CONTROL BLOCK
a->setThreshold(0.5);              // the Config itself -- NOT protected by the count

The correct model. Atomicity covers the count. Ownership transfer and destruction are safe from multiple threads; concurrent use of the pointed-to object needs its own synchronisation (a mutex, or an immutable object, or a per-thread copy).

The re-test. Two threads each hold a shared_ptr<T> and both call a non-const member on it. What does the reference count guarantee about that call? (Nothing at all.)


"A weak_ptr keeps the object alive."

Why this feels true. It is a smart pointer, it refers to the object, and use_count() looks adjacent. It reads like an owner.

Where it breaks. It is deliberately excluded from the count:

  a weak_ptr does not keep the node alive: use_count 1, expired false
  lock() succeeded while the owner existed: watched, and use_count is now 2
  the owner is gone: expired true, lock() returns null

And it cannot be used like a pointer at all — the compiler refuses:

error: could not convert 'weak' from 'std::weak_ptr<int>' to 'bool'
error: no match for 'operator*' (operand type is 'std::weak_ptr<int>')

The correct model. weak_ptr is an observer. lock() gives you a temporary owner if the object still exists; expired() tells you whether it is gone. Never check and then use — lock and then use, because between the check and the use the last owner may die.

The re-test. Why is if (!weak.expired()) { use(*weak.lock()); } still wrong, even though the check is present? (The pointer has to be locked to be dereferenced at all, and the same race applies to any check-then-use sequence.)


"Passing a unique_ptr by value is how you pass ownership."

Why this feels true. It is literally the signature that takes ownership, and it is the one introduced first.

Where it breaks. It is the right signature for a transfer and the wrong one for everything else. A callee that only reads does not need the object to move into it:

void report(const Shape& shape);            // reads: caller keeps ownership, no move at all
void take(std::unique_ptr<Shape> shape);    // takes: caller must move, and loses the object

And an lvalue cannot be passed to the second one, which is the compiler teaching the rule:

error: use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr(
       const std::unique_ptr<_Tp, _Dp>&) [with _Tp = Shape; _Dp = std::default_delete<Shape>]'
note:   initializing argument 1 of 'void takeOwnership(std::unique_ptr<Shape>)'

The correct model. Choose the signature from the callee's need: const T& to read, T& to modify, T* if absence is possible, unique_ptr<T> to take ownership, shared_ptr<T> to share it. A unique_ptr parameter is a promise that the callee keeps the object, and promises should be true.

The re-test. Count the std::move calls in your last program. Which of them were transfers, and which were a reference that was never needed?


"std::move is needed to pass a smart pointer."

Why this feels true. Moving is how ownership transfer works, and std::move is how you move. The two are learned together.

Where it breaks. std::move is only needed when a call takes ownership. For a borrowing call it is not merely unnecessary, it is a transfer you did not intend:

report(*owner);              // borrow. No move. The object is still yours.
process(std::move(owner));   // transfer. owner is now null, and better be unused afterwards.

The correct model. Read the callee's signature first. const T&, T& and T* take no ownership, and a unique_ptr cannot be passed to them at all — you must dereference or call .get() — which is the type system warning you that the ownership question has not been asked.

The re-test. What happens if you write std::move(owner) and then use owner? (It is null. On this implementation that is a null-pointer dereference at the next use; the moved-from state of a unique_ptr is guaranteed to be empty, unlike the general moved-from state of Module 10.)


"The compiler would have warned me."

Why this feels true. Modern compilers are excellent, and Modules 5 to 10 produced a steady stream of helpful diagnostics, including notes explaining why.

Where it breaks. Every bug in this module compiles cleanly:

Bug Diagnostic
shared_ptr<Node> a, b; a->next = b; b->next = a; none — neither destructor runs, at run time
std::shared_ptr<T> p(raw); std::shared_ptr<T> q(raw); none — two control blocks, two deletes
std::shared_ptr<T> p(new T) instead of make_shared none — one wasted allocation
A non-virtual base destructor with unique_ptr<Base> none — undefined behaviour when the object dies
A shared_ptr parameter where a reference would do none — one atomic operation per call

The correct model. Some mistakes are legal, and the compiler's job is legality. Ownership mistakes are mostly of this kind, which is why this module's technique is instrumenting the resource and counting, rather than reading diagnostics.

The re-test. Which of the five rows above would a sanitizer report? (Only the second, as a double free — and only if the second deletion happens while the program is still running. The cycle is reported by neither.)


Part 2 — Common programming mistakes

Symptom Cause Fix
error: use of deleted function 'unique_ptr(const unique_ptr&)' Copying a unique_ptr Move it, or take a reference if the callee does not need ownership
error: … initializing argument 1 of 'void take(unique_ptr<T>)' Passing an lvalue to an owning parameter std::move if the transfer is intended; otherwise change the parameter to T&
A long error chain ending in static assertion failed: result type must be constructible from input type Copying a container of unique_ptr Move the container, or hold shared pointers if sharing is really needed
Objects never destroyed; memory grows A shared_ptr cycle Make one link a weak_ptr, or restructure so ownership is acyclic
Double free when using this shared_ptr<T>(this) — a second control block Derive from enable_shared_from_this<T> and use shared_from_this()
A Derived object leaks or misbehaves through unique_ptr<Base> Base destructor not virtual Make it virtual — Module 7
error: invalid new-expression of abstract class type 'Shape' make_unique<Base> on an abstract base Create a concrete type: make_unique<Circle>()
error: could not convert … from 'std::weak_ptr<T>' to 'bool' Using a weak_ptr as though it were a pointer lock(), then check the result
error: conversion from 'unique_ptr<Base,…>' to non-scalar type 'unique_ptr<Derived,…>' Moving a base pointer down the hierarchy dynamic_cast to a raw pointer and wrap it with a custom deleter only if you must — usually the design is wrong
Code compiles, runs, and silently retains memory forever The cycle, or the second control block Neither the compiler nor a sanitizer will help. Instrument and count

Mistake 1 — transferring ownership by accident [L3 Modify]

Reproduce it.

void takeOwnership(std::unique_ptr<Shape> shape);

void callIt() {
    std::unique_ptr<Shape> owner = std::make_unique<Circle>();
    takeOwnership(owner);               // an lvalue: no move
}

What you see.

error: use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr(
       const std::unique_ptr<_Tp, _Dp>&) [with _Tp = Shape; _Dp = std::default_delete<Shape>]'
note: declared here
note:   initializing argument 1 of 'void takeOwnership(std::unique_ptr<Shape>)'

Why. Passing by value requires a copy to build the parameter. A unique_ptr has no copy constructor, so the compiler reports the deletion — and the note: naming the parameter is the useful half: it tells you why the copy was attempted.

Fix it. Decide which of the two you meant:

takeOwnership(std::move(owner));    // I am giving it away
// or
takeOwnership(*owner);              // change the parameter to const Shape& -- I am only looking

Prevent it. Read the signature before calling. If the transfer is not intended, the parameter is wrong, and fixing the parameter is better than adding a std::move.


Mistake 2 — copying a container of owners [L3 Modify]

Reproduce it.

std::vector<std::unique_ptr<Shape>> duplicate(const std::vector<std::unique_ptr<Shape>>& shapes) {
    return shapes;                      // a container of unique_ptr is not copyable
}

What you see. A long chain of required from frames inside the standard library, ending in:

error: static assertion failed: result type must be constructible from input type
note: 'std::integral_constant<bool, false>::value' evaluates to false

with one frame in that chain naming your line: required from here.

Why. std::vector<T>'s copy constructor copies each element; unique_ptr cannot be copied; so the container's copy constructor is ill-formed. The error surfaces inside stl_uninitialized.h because that is where the copy is attempted.

Fix it. Ask what copying a container of owners would even mean. Either the operation is a move:

std::vector<std::unique_ptr<Shape>> take(std::vector<std::unique_ptr<Shape>> shapes) {
    return shapes;                      // moved in, and moved out
}

or the elements are shared deliberately, and then the container is std::vector<std::shared_ptr<Shape>>.

Prevent it. Read a long template error from the bottom: the last required from here naming your own file is the line to change. Module 12 makes a technique of this.


Mistake 3 — make_unique on an abstract base [L2 Apply]

Reproduce it.

struct Shape {
    virtual ~Shape() = default;
    virtual double area() const = 0;
};

std::unique_ptr<Shape> makeIt() {
    return std::make_unique<Shape>();     // Shape has a pure virtual function
}

What you see.

error: invalid new-expression of abstract class type 'Shape'
note:   because the following virtual functions are pure within 'Shape':
note:     'virtual double Shape::area() const'

Why. make_unique<T> performs new T(...), and no object of an abstract type can be created — Module 8's rule, enforced at the point of allocation. The message arrives from inside std::make_unique, but the note: names the pure virtual function, so you know exactly which promise is missing.

Fix it. Create a concrete type, and let the base pointer own it:

std::unique_ptr<Shape> makeIt() {
    return std::make_unique<Circle>(2.0);
}

Prevent it. The signature unique_ptr<Base> is right; the argument to make_unique is where concrete and abstract get confused. Ask "which kind is this, exactly?" — if the answer is "any of them", the factory returns unique_ptr<Base> and chooses internally.


Mistake 4 — a reference that should have been a dangling pointer [L4 Analyze]

Reproduce it.

const Shape& firstShape(const std::vector<std::unique_ptr<Shape>>& shapes) {
    return *shapes.front();
}

What you see. Nothing, at first. The function is correct and compiles cleanly, because shapes outlives the call. The bug appears when the caller compiles a similar function against a container that does not outlive it — an empty or cleared container, a temporary, or a moved-from vector:

reading from an empty container: undefined behaviour, and a crash with no diagnostic

Why. Returning a reference to something owned by an argument is a lifetime promise: the thing you return stays alive as long as you need it. Nothing in the return type enforces it, so it is exactly the kind of promise that breaks when the code is refactored six months later.

Fix it. Return an owner, or a weak_ptr, when the caller needs a handle that outlives the call:

std::shared_ptr<Shape> firstShape(const std::vector<std::shared_ptr<Shape>>& shapes);  // keeps it alive
// or
std::weak_ptr<Shape> firstShape(const std::vector<std::shared_ptr<Shape>>& shapes);    // observable, not owning

Prevent it. Whenever a function returns a reference or pointer, write the lifetime sentence: "the caller may use this while X is alive". If you cannot write it, the signature is wrong.


Mistake 5 — the leak that has no diagnostic [L4 Analyze]

Reproduce it.

struct Node {
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> previous;        // owns the node that owns this one
    ~Node() { std::cout << "destroyed\n"; }
};

void build() {
    auto first  = std::make_shared<Node>();
    auto second = std::make_shared<Node>();
    first->next = second;
    second->next = first;
}

What you see. Nothing. Not one destroyed line, no compiler warning, no sanitizer report — and a leak-checker calling the memory still reachable:

      [graph] leaving the function: three shared_ptrs are about to die
  alive now 3   <- three nodes have leaked

Why. Each node is kept alive by the other's owning link. Neither count can reach zero, so neither destructor runs, so nothing is ever released. The memory is not unreachable and no access is invalid, which is precisely why no automated tool calls it an error.

Fix it. Break the cycle by making one direction non-owning:

struct Node {
    std::shared_ptr<Node> next;        // owns
    std::weak_ptr<Node> previous;      // observes
};

Prevent it. When you draw a relationship, mark the direction of ownership and check that the arrows form no loop. A back-reference — to a parent, to a container, to a cache — is a weak_ptr, a reference or a raw pointer, never a second owner. This is Lab 9's ownership diagram again, this time in code.


Next: Debugging — proving that cleanup happens, including for the leak that no tool reports.