5.2.11 · D3C++ Programming

Worked examples — Rule of Zero — prefer compiler-generated specials

4,734 words22 min readBack to topic

This page is the "prove it on real code" companion to the parent Rule-of-Zero note. There we derived why composing self-managing members lets the compiler write your five special functions. Here we hunt down every case class a designer can run into and work each one fully.

Before we touch code, three things we must earn:

If any of these words feel new, they are fully built in Rule of Three and Rule of Five and Move semantics and rvalue references.


The scenario matrix

The parent note gives one axis: does the class directly own a non-RAII resource? But real design has more corners than that. Here is the full grid of cases this topic can throw at you. Each cell gets at least one worked example below.

What "danger" means here — we order the cells by how much manual resource logic the case demands of you, which is exactly the thing Rule of Zero tries to drive to zero:

  • Zero danger (A, B): every member is a value or a self-managing type; you write nothing and it is correct.
  • Low danger (C, D, E): still zero written code, but you must reason about which copy/move behaviour the member hands you (and confirm the degenerate cases).
  • High danger (F, G, H): a raw resource, or a keystroke that poisons the generated set, appears — now real bugs (double free, silent slow copies) are possible unless contained.
  • Applied (I, J): put the whole ladder together in a real design and an exam trap.

So the left-to-right ordering is literally "how many ways can this bite you if you're careless" — increasing.

# Case class The question it forces Danger Example
A All value members (int, double) Is copy really "just bytes"? zero Ex 1
B Self-managing members only (vector, string) Does correctness compose? zero Ex 2
C unique_ptr member — move-only "sign" What happens to copy? low Ex 3
D shared_ptr member — shared "sign" Copy = duplicate or share? low Ex 4
E Degenerate/zero input (empty container, nullptr handle) Do the auto specials still behave? low Ex 5
F The poison member — raw owning T* Why the cascade fires high Ex 6
G No RAII wrapper exists (FILE*, OS handle) Isolate into one type high Ex 7
H The silent trap — declaring ~T(){} for logging Limiting behaviour: moves vanish high Ex 8
I Word problem — a real game/engine class Pick the member, not the specials applied Ex 9
J Exam twist — mixed members, "what does the compiler generate?" Reason about each of the five applied Ex 10

We walk this table top-to-bottom, from zero danger to applied.


Cell A — all value members


Cell B — self-managing members compose


Cell C — the unique_ptr member (move-only "sign")

Now the sign flips: a member that cannot be copied at all.


Cell D — the shared_ptr member (shared "sign")

Same member shape, opposite copy behaviour.


Cell E — degenerate / zero inputs

The parent's contract demands we cover degenerate cases: empty containers, a unique_ptr holding nullptr. Do the auto specials still behave?


Cell F — the poison member (raw owning pointer)

Here danger appears. This is why the whole rule exists.


Cell G — no RAII wrapper exists

First, the keyword that shows up in the move operations below:


Cell H — the silent trap (limiting behaviour)


Cell I — real-world word problem


Cell J — exam-style twist


The whole grid, at a glance

no

yes

yes

no

unique_ptr present

shared_ptr present

only values and containers

Does the class directly own a non-RAII resource

Rule of Zero: write nothing

Is there an RAII wrapper for it

Use the wrapper as a member then Rule of Zero

Write ONE small RAII type then Rule of Zero elsewhere

Member capabilities decide copy and move

Class is move-only copy deleted

Class is copyable copy shares

Class is copyable and movable deep

Recall Quick self-test

A class has one std::string and one std::unique_ptr<T>. Copyable? ::: No — the unique_ptr makes copy implicitly deleted, so the whole class is move-only. You add ~T() = default; "just to be safe." What breaks? ::: The implicit move operations are suppressed, so the class silently copies (or, if a member is non-copyable, becomes immovable). Raw FILE* with no wrapper — where do the specials go? ::: Into ONE small RAII class; every other class then uses Rule of Zero. Copy of a class with a shared_ptr member — deep or shared? ::: Shared — both objects point at the same target and the reference count goes up. Why mark move operations noexcept? ::: So containers actually use them; a throwing move makes vector fall back to a slow copy.