5.2.11 · D5C++ Programming

Question bank — Rule of Zero — prefer compiler-generated specials

1,531 words7 min readBack to topic

These traps target the Rule of Zero and lean on RAII, Rule of Three and Rule of Five, Move semantics and rvalue references, and the smart-pointer types.


True or false — justify

True or false: A class made only of int, std::string, and std::vector<double> members needs no hand-written special member functions.
True. Every member already knows how to copy, move, and destroy itself, so the compiler's memberwise generation composes correct behaviour — this is exactly Rule of Zero.
True or false: Adding an empty ~Widget() {} destructor is harmless because it does no resource work.
False. Declaring any destructor suppresses the implicitly generated move constructor and move assignment, so the class silently degrades to copying even though the body is empty.
True or false: Writing ~Widget() = default; avoids the move-suppression problem because = default means "act like the compiler."
False. Merely declaring the destructor — even as = default — still suppresses the implicit moves. The suppression is triggered by the declaration, not by whether you supply a body.
True or false: Rule of Zero forbids ever declaring a special member function, including a default constructor.
False. Rule of Zero forbids hand-written resource logic, not keystrokes. Declaring Widget() = default; to restore a default constructor after adding another constructor is perfectly within its spirit.
True or false: A class holding a std::unique_ptr<T> member and writing zero specials is copyable.
False. unique_ptr is not copyable, so the compiler implicitly deletes the copy operations of the enclosing class. It becomes move-only — which is usually exactly what you want.
True or false: If a member type is noexcept-movable, a Rule-of-Zero class built from it is also noexcept-movable.
True. The compiler-generated move operations are noexcept if and only if every member's corresponding move is noexcept, so the guarantee propagates automatically — see noexcept and move operations.
True or false: Replacing a raw int* member with std::vector<int> can eliminate all five special members at once.
True. vector supplies a correct destructor, deep copy, and cheap move, so the destructor you were forced to write vanishes and the whole Rule-of-Five cascade collapses to zero.
True or false: A class using std::shared_ptr<T> and no specials copies deeply.
False. Copying a shared_ptr shares ownership of the same object (bumps a reference count); it does not clone the pointee. The copy is correct but shallow-shared, per std::shared_ptr — shared ownership.

Spot the error

Spot the error: struct S { int* p; S(int n):p(new int[n]){} ~S(){ delete[] p; } }; claims to be Rule of Zero.
It is the opposite of Rule of Zero. The raw owning int* plus the hand-written ~S triggers the Rule of Five, and the still-default copy constructor now double-frees. Replace int* with std::vector<int>.
Spot the error: A developer writes only a copy constructor "for clarity" and expects moves to keep working.
Declaring a copy constructor stops the compiler from generating the move constructor and move assignment. The class silently falls back to copying — you have re-entered the Rule of Five and must now write the moves by hand too.
Spot the error: To make a Widget holding unique_ptr<Shape> copyable, a coder adds Widget(const Widget&) = default;.
= default on the copy constructor tries to copy a unique_ptr, which is non-copyable, so the defaulted copy is defined as deleted. The class is still not copyable and you've added noise. Use shared_ptr or a clone helper instead.
Spot the error: A "Logger" class embeds a raw FILE* and writes its own copy/move/destructor, spread through the class alongside business logic.
This scatters manual ownership across a business class, violating the Single Responsibility Principle. Wrap the FILE* in one small RAII FileHandle, then let Logger follow Rule of Zero.
Spot the error: class A { std::vector<int> v; A(const A& o) { v = o.v; } }; — "just being explicit."
The hand-written copy constructor is redundant (memberwise copy already does this) and it suppresses the implicit move operations, so A now copies where it could have moved. Delete the copy constructor entirely.
Spot the error: A class adds ~T() { std::cout << "bye"; } purely for debug logging and expects no performance change.
The debug destructor kills the implicit moves, so every "move" becomes a copy. Log from a scope guard or elsewhere; do not pay the move-suppression tax for a print statement.

Why questions

Why does declaring a destructor force you toward the Rule of Five rather than just adding cleanup?
A destructor usually signals manual ownership, so the default copy is now likely wrong (double-free), and post-C++11 the destructor also blocks implicit moves — so you must supply copy and move operations yourself.
Why does Rule of Zero produce correct behaviour and not merely some behaviour?
Because it composes the already-correct copy/move/destroy of each member. The compiler simply calls each member's own vetted operation, so correctness is inherited rather than re-implemented and re-bugged.
Why is a move-only class (via unique_ptr) often the desired outcome rather than a limitation?
Unique ownership means "exactly one owner," so a copy would be semantically meaningless. The compiler deleting copy for you enforces the intended single-ownership contract with zero =delete typing.
Why should the manual-resource logic live in one tiny wrapper instead of the class that uses it?
Isolating manual new/delete/handle logic into one small, audited RAII type means only that type risks ownership bugs. Every consumer stays a clean Rule-of-Zero class, applying single responsibility.
Why does memberwise copy suffice for value types but fail for a raw owning pointer?
For a value type, copying the bytes yields an independent equal object. For a raw owning pointer, memberwise copy duplicates the address, so two objects share (and both later free) one buffer — a double-free.
Why does the copy-and-swap idiom belong to the Rule-of-Five world, not Rule of Zero?
Copy-and-swap is a technique for writing a correct assignment when you must write specials at all. Under Rule of Zero you write no assignment operator, so the idiom simply never arises.

Edge cases

Edge case: A class has zero data members and no user-declared specials — what does the compiler generate?
All special members are generated and trivial. It copies, moves, and destroys as no-ops, so it fully satisfies Rule of Zero by default.
Edge case: A member is std::shared_ptr<T> and you copy the object twice, then destroy one copy — is the pointee freed?
No. The reference count drops but stays above zero while other copies live. The pointee is freed only when the last shared owner is destroyed, so the surviving copies remain valid.
Edge case: You add a non-default constructor to a Rule-of-Zero class and lose the default constructor — does this break Rule of Zero?
No. Declaring T() = default; to bring back the default constructor writes no resource logic, so you remain within Rule of Zero — "zero hand-written ownership," not "zero declarations."
Edge case: A member is const or a reference — what happens to the generated copy/move assignment?
Copy assignment and move assignment are implicitly deleted, because you cannot re-assign a const member or rebind a reference. The class stays constructible/copyable but not assignable — and you wrote none of that yourself.
Edge case: A member type has a deleted move constructor but a usable copy — can the enclosing Rule-of-Zero class still be "moved"?
Yes, but the "move" falls back to copying that member, since a move that can't move safely copies instead. The class still moves overall; it just isn't cheap for that member.
Edge case: One member's move constructor is not noexcept — how does that affect the class inside a std::vector?
The class's generated move becomes non-noexcept, so std::vector growth will copy rather than move elements to preserve its strong exception guarantee — a silent performance loss traced back to that one member (see noexcept and move operations).