5.2.10 · D5C++ Programming
Question bank — Move constructor and move assignment — Rule of Five
Before we start, a shared vocabulary so nothing below is unexplained:
For the full derivation of the five members see the parent: Move constructor & move assignment.
True or false — justify
std::move(x) moves the object x.
False. It performs nothing at runtime — it is only a compile-time cast of
x to an rvalue reference. The actual stealing happens later inside whatever move constructor or move assignment gets selected because of that cast. See std::move and rvalue references.After Buffer c = std::move(a); the variable a is destroyed and unusable.
False.
a still exists and its destructor still runs; it is left in a valid moved-from (empty) state. You may safely destroy or reassign it — you just shouldn't read its old contents.Declaring only a destructor is safe because the compiler auto-generates correct copy and move members.
False. Declaring a destructor suppresses the implicit move members and the implicit copy does a shallow pointer copy → two owners → double free. This is exactly why the Rule of Five exists.
A move constructor should be marked noexcept, but marking move assignment noexcept is pointless.
False. Both should be
noexcept. Containers like std::vector inspect both when deciding whether to move or copy during reallocation; only noexcept moves preserve the strong exception guarantee (Exception safety guarantees).If you use std::vector or std::string members instead of raw pointers, you can = default all five members.
True. Those members manage their own resources (RAII), so the compiler-generated moves and copies are already correct — this is the Rule of Zero: write none of the five yourself.
A move is always faster than a copy.
False in general. A move is faster only when there's a resource to steal (O(1) pointer swap vs O(N) deep copy). For a type holding just a few
ints by value, move and copy cost the same.Returning a local object by value forces a move constructor call.
False. The compiler often applies copy elision / RVO and constructs the object directly in the caller — neither copy nor move runs. See Copy elision and RVO.
The self-assignment guard if (this != &o) in move assignment is unnecessary defensive coding.
False. Generic algorithms (e.g.
std::sort, swapping equal elements) can produce x = std::move(x). Without the guard you delete[] your own buffer and then read a dangling pointer.Spot the error
Buffer(Buffer&& o) : data_(o.data_), size_(o.size_) {} — what's missing?
It steals
o.data_ but never sets o.data_ = nullptr. Now both objects point at one buffer, and o's destructor frees it — a double free. Rule: steal, then sterilize.Buffer& operator=(Buffer&& o) { data_ = o.data_; size_ = o.size_; o.data_ = nullptr; return *this; } — what leaks?
It never
delete[] data_ before overwriting the pointer, so the object's previous buffer is leaked. Move assignment must free the current resource first.Buffer(Buffer&& o) { data_ = o.data_; } (no noexcept).
The
noexcept is missing, so std::vector reallocation will fall back to copying elements instead of moving them — the move optimization silently disappears in containers.Buffer(const Buffer&& o) used as a move constructor.
const Buffer&& binds an rvalue but forbids modifying o, so you cannot null out its pointer — you'd be forced into a copy. A real move constructor takes a non-const Buffer&&.Inside a move constructor: data_ = std::move(o.data_); where data_ is a raw int*.
std::move on a raw pointer does nothing useful — pointers are copied trivially. The real work (o.data_ = nullptr;) is still missing, so this still double-frees.Buffer make() { Buffer b(1000); return std::move(b); } — why is this a pessimization?
Wrapping the return in
std::move disables copy elision/RVO, forcing an actual move where the compiler would otherwise construct in place for free. Return the named local directly.Why questions
Why does a move need to null out the source rather than just copying the pointer?
So the source's destructor doesn't
delete[] the buffer the destination now owns. Without nulling, one buffer has two owners and gets freed twice.Why does declaring a copy constructor suppress the implicit move constructor?
The language assumes that if you cared enough to write special copy behaviour, the default member-wise move is probably unsafe for your type — so it withholds it, forcing you to think about the Rule of Five.
Why must move operations leave the source valid rather than in any broken state?
Because the source object still has a lifetime — its destructor will run, and it may be reassigned. A valid empty state (pointer
nullptr, size 0) makes both operations safe.Why does std::vector care whether your move constructor is noexcept?
During reallocation it must preserve the strong guarantee: if moving element 5 threw, already-moved elements 0–4 couldn't be restored. A
noexcept move can't throw, so it's safe to use; otherwise the vector copies. See Exception safety guarantees.Why is std::move only a cast and not an operation that steals?
Because C++ separates the two concerns:
std::move merely marks a value as "an rvalue you may plunder"; the plundering logic lives in the type's own move constructor/assignment. This keeps the cast trivial and reusable for any type.Why does copy-and-swap give the copy assignment strong exception safety?
The risky allocation happens inside a temporary via the copy constructor before touching
*this. If it throws, *this is untouched. Only after the copy succeeds do we swap (which is noexcept).Why does the Rule of Zero exist if the Rule of Five is already correct?
Because writing five members by hand is error-prone. If your members are RAII types (like Smart pointers (unique_ptr / shared_ptr)), the compiler-generated five are already correct — so writing zero is safer than writing five.
Edge cases
What is x = std::move(x) (self-move) and what protects against it?
A self-move assignment, reachable via generic algorithms. The
if (this != &o) guard prevents deleting your own buffer and then reading the freed pointer.Buffer(size_t n) with n == 0 — is new int[0] valid?
Yes.
new int[0] returns a non-null, unique pointer that you must still delete[]. Zero size is a legal degenerate case, not undefined behaviour — the destructor still runs correctly.Moving from a const object — what actually happens?
A
const lvalue can't bind to Buffer&&, so overload resolution silently selects the copy constructor instead. You get a deep copy, not a move — no error, just lost optimization.After a move, is calling .size() on the moved-from object undefined behaviour?
No — it's defined but returns the unspecified moved-from value (here
0 because we set it). The object is valid; reading it is legal but relying on old contents is a logic bug.Double-move: Buffer c = std::move(a); Buffer d = std::move(a); — is the second move UB?
No.
a is already empty (pointer nullptr, size 0), so the second move just steals "nothing" and leaves a empty again. It's wasteful and pointless, but well-defined.If a class holds a unique_ptr member and nothing else raw, how many of the five must you write?
Zero.
unique_ptr is move-only and RAII: the defaulted move ctor/assign transfer it, and the copy members are auto-deleted so accidental copies won't compile. This is the Rule of Zero.Can a type be movable but not copyable? Give the canonical example.
Yes —
std::unique_ptr deletes its copy members but defines move members. It expresses exclusive ownership: you may transfer it, never duplicate it.Recall Rapid self-test (cover the answers)
Every trap above reduces to three habits. Habit that prevents double-free ::: In a move, null out the source after stealing its pointer. Habit that prevents container copy fallback ::: Mark both move members noexcept. Habit that prevents dangling-pointer self-destruction ::: Keep the self-assignment guard in move assignment.
Connections
- Parent: Rule of Five — the derivation these traps test
- Rule of Three — the pre-move ancestor these traps compare against
- Rule of Zero — how to sidestep most traps entirely with RAII members
- std::move and rvalue references — why
std::moveis a cast, not a move - Copy elision and RVO — the "neither copy nor move" edge case
- Smart pointers (unique_ptr / shared_ptr) — movable-not-copyable canonical type
- Exception safety guarantees — why
noexceptand copy-and-swap matter