C++ Learning

第34回 Rule of 0/3/5

クラスに特殊メンバ関数(デストラクタ・コピー 2 つ・ムーブ 2 つの計 5 つ)を書くときの設計原則。結論から言うと「Rule of Zero(何も書かない)」を狙うのが現代 C++ のベストプラクティスです。書かざるを得ないときは 5 つセットで揃える。この判断フローを整理します。

このページで押さえること
✅ 最低限ここだけ覚える
  • 特殊メンバ関数は5 つ:デストラクタ、コピー×2、ムーブ×2
  • 全部書かない(Rule of Zero)が最もモダン
  • 書く必要があるときは5 つ全部揃える(Rule of Five)
  • メンバを STL 型にラップするとほぼ Rule of Zero になる
⭐ 余裕があれば読む
  • 1 つ書くと他が暗黙生成されないルール
  • C++03 時代の Rule of Three
  • = default の効果
  • 継承階層で仮想デストラクタが必要なケース

1. まず触ってみる ― 特殊メンバ関数 5 つ

C++ のクラスには5 つの「特殊メンバ関数」があります。何も書かなくてもコンパイラが(条件を満たせば)自動生成してくれる関数群です。

#関数シグネチャ呼ばれるタイミング
1デストラクタ~Foo()オブジェクトの破棄時
2コピーコンストラクタFoo(const Foo&)既存から新規作成(コピー)
3コピー代入Foo& operator=(const Foo&)既存に代入(コピー)
4ムーブコンストラクタFoo(Foo&&)既存から新規作成(ムーブ)
5ムーブ代入Foo& operator=(Foo&&)既存に代入(ムーブ)

3 つの流儀

RULE OF ZERO

0 個書く(推奨)

  • 5 つ全部コンパイラ任せ
  • メンバを STL 型(vector / string / unique_ptr)にする
  • 最もモダンで保守しやすい
推奨
RULE OF THREE

3 つ書く(C++03 時代)

  • デストラクタ・コピー 2 つ
  • C++11 以前はムーブが存在しない
  • 現代では使わない
歴史
RULE OF FIVE

5 つ全部書く

  • 自作 RAII クラスなど必要な場合
  • 5 つをセットで揃えることで一貫性を保つ
  • Rule of Zero で済ませられるならそちらを優先
必要時
ここまでで覚えること:
  • 特殊メンバ関数 = 5 つ
  • 現代 C++ では書かない(Rule of Zero)が原則
  • 書く必要があるときは5 つ全部書く(Rule of Five)

2. Rule of Zero(推奨)

リソース管理を STL 型に任せれば、自分のクラスでは特殊メンバ関数を書かなくてよい。これが Rule of Zero。Scott Meyers が推奨する「最も安全で最も楽」な流儀。

昔のやり方手動
class BadBuffer { int* data_; size_t size_; public: BadBuffer(size_t n) : data_(new int[n]), size_(n) {} ~BadBuffer() { delete[] data_; } BadBuffer(const BadBuffer&); // copy ctor BadBuffer& operator=(const BadBuffer&); // copy assign BadBuffer(BadBuffer&&) noexcept; // move ctor BadBuffer& operator=(BadBuffer&&) noexcept; // move assign // 5 つすべて自分で実装… };
Rule of Zero推奨
class GoodBuffer { std::vector<int> data_; public: explicit GoodBuffer(size_t n) : data_(n) {} // 以下、何も書かない // デストラクタ:vector が自動で delete[] // コピー:vector が深いコピー // ムーブ:vector がポインタの横取り // すべて暗黙生成で完璧 };

右側の設計は、「リソース管理の責任を std::vector に委譲」しています。自分のクラスはコンパイラ生成版が自動的に正しい挙動をするので、書く必要がありません。

Rule of Zero を満たすために:
  • 動的メモリ → std::vector / std::string / std::unique_ptr / std::shared_ptr
  • ファイル → std::fstream
  • mutex ロック → std::lock_guard
「生のリソース(malloc / fopen / pthread_mutex_lock)を直接メンバに持たない」のが極意。

3. Rule of Five

どうしても自分でリソース管理が必要な場合(unique_ptr を自作するような超低レベルなケース)は、5 つ全部を揃えて書く

Rule of Five自作 RAII
class MyUniquePtr { int* p_ = nullptr; public: explicit MyUniquePtr(int* p = nullptr) : p_(p) {} // ① デストラクタ ~MyUniquePtr() { delete p_; } // ② ③ コピーは禁止(所有権は 1 つしか持てない) MyUniquePtr(const MyUniquePtr&) = delete; MyUniquePtr& operator=(const MyUniquePtr&) = delete; // ④ ムーブコンストラクタ(横取り) MyUniquePtr(MyUniquePtr&& other) noexcept : p_(other.p_) { other.p_ = nullptr; } // ⑤ ムーブ代入 MyUniquePtr& operator=(MyUniquePtr&& other) noexcept { if (this != &other) { delete p_; // 古い中身を解放 p_ = other.p_; // 横取り other.p_ = nullptr; } return *this; } int& operator*() { return *p_; } };

このクラスは「所有権は常に 1 つ」というポリシーを、コピー禁止 + ムーブだけ可能という特殊メンバ関数の組み合わせで表現しています。std::unique_ptr はまさにこの設計。

4. 暗黙生成の落とし穴

5 つの特殊メンバ関数には、1 つ書くと他が自動生成されなくなるルールがあります。これが最も混乱を招くポイント。

自分で定義したもの暗黙生成されなくなるもの
デストラクタムーブ 2 つが非生成(コピーはまだ生成)
コピーコンストラクタムーブ 2 つが非生成
コピー代入ムーブ 2 つが非生成
ムーブコンストラクタコピー 2 つ + ムーブ代入が非生成
ムーブ代入コピー 2 つ + ムーブコンストラクタが非生成
よくある罠: 「デバッグ用に ~Foo() を書いてログ出力した途端、ムーブが効かなくなって vector への挿入が遅くなった」― というよくあるバグ。ムーブが非生成になったことで、すべてコピーにフォールバックしたため。

この問題を避けるには:

= default で復活明示
class Foo { public: ~Foo() { /* 何か特別な処理 */ } // ↑ デストラクタを定義したのでムーブが消える // 取り戻すため = default で明示的に復活させる Foo(const Foo&) = default; Foo& operator=(const Foo&) = default; Foo(Foo&&) noexcept = default; Foo& operator=(Foo&&) noexcept = default; };
ここまでで基本は OK
最後の §5 に判断フローを書きます。実戦で迷ったら参照してください。

5. 判断フローチャート

Q1: 自作のクラスは生のリソース(malloc / FILE* / mutex など)を直接メンバに持っている?
❌ YES → 持たない設計に変える(vector/unique_ptr/fstream/lock_guard にラップ)
✅ NO → Q2 へ
Q2: メンバは全部 STL 型 / 値型 / 他のクラス(すでに RAII )?
✅ YES → Rule of Zero:何も書かない(完成)
❌ NO → Q3 へ
Q3: 自作 RAII を本当に書く必要がある?
✅ YES → Rule of Five:5 つ全部書く
❌ NO(多くの場合) → Rule of Zero へ戻る
実務の 95% は Rule of Zero で済む: int, double, std::string, std::vector, std::unique_ptr など「自分で特殊メンバ関数を書かなくていいメンバ」だけで構成すれば、コンパイラが全部自動で正しい版を生成してくれます。

リソース管理は他人に任せる」― これが現代 C++ のエレガントさの核心です。
広告スペース

確認クイズ

Rule of 0/3/5 を 4 問で確認。

Q1. 特殊メンバ関数は合計何つ?

3 つ(デストラクタ、コピー 2 つ)
4 つ(デストラクタ、コピー、ムーブ、代入)
5 つ(デストラクタ、コピー 2 つ、ムーブ 2 つ)
6 つ(上記 + コンストラクタ)
5 つ:デストラクタ、コピーコンストラクタ、コピー代入、ムーブコンストラクタ、ムーブ代入。これに通常のコンストラクタ(デフォルト含む)を加えて「6 つ」と数える流派もあります。

Q2. Rule of Zero の最大の利点は?

コンパイルが速くなる
特殊メンバ関数を書かなくて済み、バグの余地が減る
実行時間が短縮される
継承が不要になる
Rule of Zero = メンバをすべて STL 型(など既にリソース管理された型)にして、自分では特殊メンバを書かない。書く量が減る = バグが減る + 保守しやすい。コンパイル速度や実行速度への直接的な効果は無い。

Q3. 次のコードでムーブコンストラクタが暗黙生成されないのはどれ?

class Foo { int x; };
class Foo { ~Foo(){ log("dtor"); } };
class Foo { int x = 0; };
class Foo { std::vector<int> v; };
デストラクタを自分で書くと、ムーブ 2 つが暗黙生成されなくなります。これを取り戻すには = default で明示的に書く必要があります。この罠が「Rule of Zero を守れ」の最大の理由。

Q4. std::unique_ptr 的な「所有権 1 つ」型を作るとき、正しいパターンは?

コピーとムーブの両方を許す
コピーとムーブの両方を禁止
コピーを禁止、ムーブのみ許可
ムーブを禁止、コピーのみ許可
所有権が1 つしかない型は「コピーすると所有者が複数になる」のでコピー禁止。しかし所有権の移動は許したい(ムーブ)。std::unique_ptr はまさにこの設計です。