Exercises — Rule of Zero — prefer compiler-generated specials
Before we count anything, let us fix the vocabulary so no symbol appears unearned.
The scoreboard we use everywhere below is a single count, drawn as a dial.

One more fact used constantly, so we state it once and picture it.

Level 1 — Recognition
L1.1
State whether this class obeys Rule of Zero (writes zero specials correctly). How many specials will the compiler generate for it?
struct Point { int x; int y; };Recall Solution
Both members are int — plain value types, no resource. Memberwise copy/move/destroy is correct. So Rule of Zero holds. The compiler generates all 5 specials (plus the default constructor). Hand-written specials needed: 0.
L1.2
For each member type, answer "does the class directly own a non-RAII resource?"
(a) std::string name; (b) int* buf; (built with new) (c) std::unique_ptr<Node> next; (d) FILE* f;
Recall Solution
(a) No — std::string is RAII, self-managing → Rule of Zero.
(b) Yes — raw owning pointer, non-RAII → forces Rule of Five.
(c) No — unique_ptr is RAII → Rule of Zero.
(d) Yes — FILE* has no standard RAII wrapper → must wrap it.
Count of members that break Rule of Zero: 2 (b and d).
L1.3
Rule of Zero means I must literally never type any special-function keyword. True or false?
Recall Solution
False. Rule of Zero means zero hand-written resource logic (no bodies babysitting a raw resource). Writing T() = default; to keep a default constructor after adding another constructor is perfectly within the spirit — you wrote no ownership code.
Level 2 — Application
L2.1
This class compiles. Exactly how many of the five specials are user-declared, and how many does the compiler generate?
class Log {
std::vector<std::string> lines;
public:
Log() = default;
void add(std::string s) { lines.push_back(std::move(s)); }
};Recall Solution
Log() is a default constructor, which is not one of the five specials — it does not trip the suppression rule. So:
- User-declared specials (of the five): 0.
- Compiler-generated specials: 5 (dtor, copy-ctor, copy-assign, move-ctor, move-assign), all correct via
vector<string>. Adding a member function likeaddnever affects special generation.
L2.2
Refactor to Rule of Zero and count the lines you deleted.
class Buffer {
int* data; size_t n;
public:
Buffer(size_t n) : data(new int[n]), n(n) {}
~Buffer() { delete[] data; }
Buffer(const Buffer& o) : data(new int[o.n]), n(o.n) { std::copy(o.data,o.data+n,data); }
Buffer& operator=(const Buffer&); // copy-and-swap body elsewhere
Buffer(Buffer&& o) noexcept : data(o.data), n(o.n) { o.data=nullptr; }
Buffer& operator=(Buffer&&) noexcept;
};Recall Solution
Replace the raw int* + size_t with a self-managing std::vector<int>:
class Buffer {
std::vector<int> data;
public:
explicit Buffer(size_t n) : data(n) {}
};We removed the 5 declared specials. Counting the special-function lines shown (dtor, copy-ctor, copy-assign, move-ctor, move-assign = 5 declarations), all are gone. Hand-written specials: from 5 → 0.
Level 3 — Analysis
L3.1
A teammate adds a "harmless" logging destructor:
class Session {
std::vector<char> buf;
public:
~Session() { std::cout << "Session closed\n"; }
};Which specials does the compiler now generate, and what performance bug appears?
Recall Solution
Declaring any destructor trips the suppression gate: the compiler no longer generates the move constructor and move assignment. It still generates copy-ctor and copy-assign.
- Generated: dtor (user's), copy-ctor, copy-assign = the class becomes copy-only.
- Bug: every "move" of a
Sessionnow silently deep-copies the wholevector<char>— potentially huge and slow. See the gate figure. Fix: delete the logging dtor, or explicitly= defaultall five to restore moves (but prefer to log elsewhere).
L3.2
Is Widget copyable? movable? Explain from the members, no compiler needed.
class Widget {
std::unique_ptr<Shape> shape;
std::string label;
};Recall Solution
unique_ptr is movable but not copyable; std::string is both.
- Copy: the implicit copy-ctor would need to copy every member. Since
unique_ptr's copy is deleted, the compiler implicitly deletesWidget's copy-ctor and copy-assign. SoWidgetis not copyable. - Move: both members are movable, so the compiler generates a correct move-ctor/move-assign.
Widgetis movable. Result:Widgetis move-only, exactly as intended, with 0 hand-written specials.
L3.3
Count the correctly-working specials the compiler provides for
class Cache { std::shared_ptr<Table> t; int hits; };Recall Solution
shared_ptr is copyable (bumps refcount) and movable; int is trivially both. No member disables anything. So the compiler generates all 5 specials, all correct: copy shares ownership, move transfers it, destroy decrements the count. Hand-written: 0.
Level 4 — Synthesis
L4.1
You must wrap a raw FILE* (no standard RAII exists). Design the minimum so that a Logger holding it can itself follow Rule of Zero. How many classes end up writing specials, and how many specials in total?
Recall Solution
Isolate the danger in one RAII type (Single Responsibility Principle):
class FileHandle { // the ONLY class with specials
FILE* f;
public:
explicit FileHandle(const char* p) : f(std::fopen(p,"r")) {}
~FileHandle() { if (f) std::fclose(f); }
FileHandle(FileHandle&& o) noexcept : f(o.f) { o.f = nullptr; }
FileHandle& operator=(FileHandle&&) noexcept;
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
};
class Logger {
FileHandle h; // Rule of Zero here!
std::string name;
}; // Logger writes ZERO specials; move-only, copy disabled — inherited from FileHandle.- Classes writing specials: 1 (
FileHandle). - Specials
FileHandledeclares: dtor, move-ctor, move-assign, deleted copy-ctor, deleted copy-assign = 5. - Specials
Loggerwrites: 0.Loggeris move-only becauseFileHandleis.
L4.2
FileHandle's move ops are marked noexcept. Why does that keyword matter for a std::vector<Logger>?
Recall Solution
When a vector grows it reallocates. It will move existing elements into new storage only if the move is noexcept — otherwise it copies them to preserve the strong exception guarantee. But Logger/FileHandle are non-copyable! So without noexcept, vector<Logger> growth would fail to compile (no copy available) or be forced down the copy path. Marking moves noexcept lets the vector move safely. See noexcept and move operations.
Level 5 — Mastery
L5.1
A tree node wants to deep-copy its whole subtree, yet you refuse to hand-write a copy constructor. Which member type gives copyable value semantics while keeping Rule of Zero, and which gives shared semantics instead? Contrast the two.
Recall Solution
- Value (deep) copy, Rule of Zero: use a value-semantic wrapper that clones on copy (e.g. a "polymorphic value" type, or store
std::vector<Node>directly so children copy deeply). Copy produces an independent subtree; still 0 hand-written specials because the member's own copy is deep. - Shared semantics: use
std::shared_ptr<Node>. Copy is cheap and shares the same children (refcount++), not an independent subtree. The choice of member dictates copy behaviour — you never write the copy constructor; you pick it. Compare with Rule of Three and Rule of Five, where you'd hand-craft the deep copy instead.
L5.2 (capstone count)
Given this design, total the hand-written specials across all classes, and state whether the whole system still counts as "Rule of Zero throughout."
struct Vec3 { double x,y,z; };
class Mesh { std::vector<Vec3> verts; std::string name; };
class GpuBuf { unsigned id; ~GpuBuf(){ glDelete(id);} GpuBuf(GpuBuf&&) noexcept; /* +2 deleted copies, +1 move-assign */ };
class Model { Mesh mesh; std::unique_ptr<GpuBuf> gpu; };Recall Solution
Vec3: threedoubles → 0 hand-written.Mesh:vector+string, both RAII → 0.GpuBuf: rawunsignedOS-like handle → must write specials: dtor + move-ctor + move-assign + 2 deleted copies = 5 declared.Model: members areMesh(Rule of Zero) andunique_ptr<GpuBuf>(RAII) → 0. Becauseunique_ptris move-only,Modelis move-only automatically. Total hand-written specials across the system: 5, all confined to the single resource-owningGpuBuf. Every other class writes 0. Verdict: the system is "Rule of Zero throughout" in the intended sense — resource logic is isolated to exactly one audited type, and no business class writes a special. This is Rule of Zero + Single Responsibility working together.
Recall One-line self-test
The reflex ::: Ask "does this class directly own a non-RAII resource?" — if no, write zero specials. The gate ::: Declaring any of {dtor, copy-ctor, copy-assign} silently deletes the free move-ctor and move-assign.