Menu
CoddyTech

Rule of Three / Five / Zero

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

クラスが動的メモリなどのリソースを管理する場合、カスタムデストラクター、コピーコンストラクター、ムーブコンストラクターが必要だと学びました。しかし、どの特殊メンバー関数を実装するかを決めるのに役立つ指針があります。それが三、五、ゼロのルールです。

三の法則では、次の3つのうちいずれかを定義する場合は、3つすべてを定義するべきだとされています。

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

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. ムーブ代入
};

どちらの代入演算子も同じパターンに従います。自己代入を防ぎ、古いリソースを解放し、新しいリソースを受け取り、*thisを返します。

Buffer& Buffer::operator=(const Buffer& other) {   // コピー代入
    if (this != &other) {
        delete[] data;
        size = other.size;
        data = new int[size];
        std::copy(other.data, other.data + size, data);
    }
    return *this;
}

Buffer& Buffer::operator=(Buffer&& other) noexcept {   // ムーブ代入
    if (this != &other) {
        delete[] data;
        data = other.data;
        size = other.size;
        other.data = nullptr;
        other.size = 0;
    }
    return *this;
}

ゼロの法則は最もシンプルな方法です。クラスがリソースを直接管理しない場合、これらの関数はどれも定義しないでください。コンパイラに生成させるか、リソースを管理してくれるスマートポインターや標準コンテナを使いましょう。

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

これらのルールに従うことで、一部の操作は定義されているのにほかの操作が欠けている場合に発生する、二重削除、メモリリーク、ダングリングポインターなどのバグを防げます。

challenge icon

チャレンジ

簡単

動的に確保された文字データを適切に管理するため、5つの特殊メンバー関数をすべて実装した「Rule of Five」に従うTextBufferクラスを作成しましょう。これにより、コピー操作とムーブ操作がどのように連携して、堅牢なリソース管理クラスを実現するかを示します。

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

  • TextBuffer.h: テキストを動的に確保された文字配列に格納するTextBufferクラスを定義します。このクラスには、次の要素が必要です。
    • プライベートメンバー: テキストの内容を格納するdataという名前のchar*ポインターと、文字列の長さ(ヌル終端文字を含まない)を格納する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()メソッド(dataが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つの関数が連携して安全なリソース管理を実現する「Rule of Five」の動作を示せます。

strlenやstrcpyなどの文字列関数には<cstring>を、std::move()には<utility>を含めてください。

自分で試してみよう

#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腕試し

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

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

自分で練習してみよう: C++オンラインコンパイラ