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:
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:
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:
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.