Menu
Coddy logo textTech

Rule of Three / Five / Zero

CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 25/104。

クラスが動的メモリのようなリソースを管理する場合、カスタムのデストラクタ、コピーコンストラクタ、およびムーブコンストラクタが必要であることを学びました。しかし、どの特殊メンバ関数を実装すべきかを判断するのに役立つ指針があります。それが、3のルール、5のルール、0のルール (Rule of Three, Five, and Zero)です。

3つのルール (Rule of Three)は、以下の3つのうちいずれかを定義する場合、これらすべてを定義すべきであると述べています:

  • デストラクタ
  • コピーコンストラクタ
  • コピー代入演算子

Rule of Five(5つのルール)は、これをモダンC++向けに拡張し、ムーブ操作を追加したものです:

  • デストラクタ
  • コピーコンストラクタ
  • コピー代入演算子
  • ムーブコンストラクタ
  • ムーブ代入演算子
class Buffer {
    int* data;
    size_t size;
public:
    Buffer(size_t s) : size(s), data(new int[s]) {}
    
    ~Buffer() { delete[] data; }                          // 1. デストラクタ
    Buffer(const Buffer& other);                          // 2. コピーコンストラクタ
    Buffer& operator=(const Buffer& other);               // 3. コピー代入演算子
    Buffer(Buffer&& other) noexcept;                      // 4. ムーブコンストラクタ
    Buffer& operator=(Buffer&& other) noexcept;           // 5. ムーブ代入演算子
};

Rule of Zero(ゼロの規則)は、最もシンプルなアプローチです。クラスがリソースを直接管理しないのであれば、これらの関数を一切定義しないでください。コンパイラに生成を任せるか、リソースを自動的に管理してくれるスマートポインタや標準コンテナを使用しましょう。

class Player {
    std::string name;              // std::string は自身のメモリを管理します
    std::vector<int> scores;       // std::vector は自身のリソースを処理します
public:
    Player(std::string n) : name(n) {}
    // デストラクタ、コピー、またはムーブ関数は不要です!
};

これらのルールに従うことで、一部の操作が定義されている一方で他の操作が欠落している場合に発生する、二重解放、メモリリーク、ダングリングポインタといったバグを防ぐことができます。

challenge icon

チャレンジ

簡単

動的に割り当てられた文字データを適切に管理するために、五則(Rule of Five)に従った TextBuffer クラスを構築しましょう。これは、5つの特殊メンバ関数すべてを実装することで、コピー操作とムーブ操作がどのように連携して堅牢なリソース管理クラスを作成するかを示すものです。

コードを整理するために、2つのファイルを作成します。

  • TextBuffer.h: 動的に割り当てられた文字配列にテキストを格納する TextBuffer クラスを定義します。クラスには以下が必要です:
    • プライベートメンバ:テキスト内容のための char* ポインタ data、および文字列の長さ(ヌル終端文字を含まない)のための size_t length
    • C言語形式の文字列(const char*)を受け取り、メモリを割り当て、内容をコピーし、"TextBuffer created: <text>" と出力する引数付きコンストラクタ
    • メモリを解放し(nullでない場合)、"TextBuffer destroyed" と出力するデストラクタ
    • ディープコピーを実行し、"TextBuffer copied" と出力するコピーコンストラクタ
    • 自己代入を処理し、既存のデータをクリーンアップし、ディープコピーを実行して、"TextBuffer copy-assigned" と出力するコピー代入演算子*this を返します
    • 所有権を移譲し、"TextBuffer moved" と出力するムーブコンストラクタnoexcept を指定)。移動元を有効な空の状態にします
    • 自己代入を処理し、既存のデータをクリーンアップし、所有権を移譲して、"TextBuffer move-assigned" と出力するムーブ代入演算子noexcept を指定)。*this を返します
    • 格納されているテキストを返す getText() メソッド(データが null の場合は空文字列 "" を返す)
    • 長さを返す getLength() メソッド
  • main.cpp: 5つの特殊メンバ関数すべての動作を実演します。入力からテキスト文字列を読み取り、以下の操作を行います:
    • 入力テキストを使用して original という名前の TextBuffer を作成する
    • original からコピーコンストラクタを使用して copied を作成する
    • テキスト "Temporary" を使用して another を作成する
    • コピー代入を使用する:another = original
    • std::move() を使用して original からムーブコンストラクトにより moved を作成する
    • テキスト "Target" を使用して target を作成する
    • ムーブ代入を使用する:target = std::move(copied)
    • "--- Final State ---" と出力する
    • 各バッファ(original, copied, moved, another, target)について、"original: <text> (length: <len>)" の形式で出力する

ムーブ後、移動元のオブジェクト(original および copied)は長さ 0 の空のテキストを表示し、移動先のオブジェクトは転送されたデータを保持する必要があります。これにより、安全なリソース管理を保証するために5つの関数すべてが連携して機能する「五則」の実践が示されます。

strlenstrcpy などの文字列関数のために <cstring> を、std::move() のために <utility> をインクルードしてください。

チートシート

三則 (Rule of Three) は、これら3つの特殊メンバ関数のいずれかを定義する場合、3つすべてを定義すべきであるという原則です:

  • デストラクタ
  • コピーコンストラクタ
  • コピー代入演算子

五則 (Rule of Five) は、ムーブ操作を追加することで、モダンC++向けにこれを拡張したものです:

  • デストラクタ
  • コピーコンストラクタ
  • コピー代入演算子
  • ムーブコンストラクタ
  • ムーブ代入演算子
class Buffer {
    int* data;
    size_t size;
public:
    Buffer(size_t s) : size(s), data(new int[s]) {}
    
    ~Buffer() { delete[] data; }                          // 1. デストラクタ
    Buffer(const Buffer& other);                          // 2. コピーコンストラクタ
    Buffer& operator=(const Buffer& other);               // 3. コピー代入
    Buffer(Buffer&& other) noexcept;                      // 4. ムーブコンストラクタ
    Buffer& operator=(Buffer&& other) noexcept;           // 5. ムーブ代入
};

零則 (Rule of Zero) は、クラスがリソースを直接管理しないのであれば、特殊メンバ関数を一切定義すべきではないという原則です。リソースを自動的に処理するスマートポインタや標準コンテナを使用します:

class Player {
    std::string name;              // std::string は自身のメモリを管理します
    std::vector<int> scores;       // std::vector は自身のリソースを処理します
public:
    Player(std::string n) : name(n) {}
    // デストラクタ、コピー、ムーブ関数は不要です!
};

これらの規則に従うことで、二重解放、メモリリーク、ダングリングポインタなどのバグを防ぐことができます。

自分で試してみよう

#include <iostream>
#include <string>
#include <utility>
#include "TextBuffer.h"

using namespace std;

int main() {
    string input;
    getline(cin, input);

    // TODO: 入力テキストを使用して 'original' という名前の TextBuffer を作成する

    // TODO: 'original' からコピーコンストラクタを使用して 'copied' を作成する

    // TODO: "Temporary" というテキストで 'another' を作成する

    // TODO: コピー代入を使用する: another = original

    // TODO: std::move() を使用して 'original' からムーブ構築により 'moved' を作成する

    // TODO: "Target" というテキストで 'target' を作成する

    // TODO: ムーブ代入を使用する: target = std::move(copied)

    // TODO: "--- Final State ---" を出力する

    // TODO: 各バッファの状態を次の形式で出力する:
    // "original: <text> (length: <len>)"
    // 対象: original, copied, moved, another, target

    return 0;
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン