Menu
CoddyTech

Rule of Three / Five / Zero

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

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

三の規則では、次の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. ムーブ代入
};

どちらの代入演算子も同じパターンに従います。自己代入を防ぎ、古いリソースを解放し、新しいリソースを受け取り、*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;
}

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 クラスを定義します。クラスには次のものが必要です。
    • private メンバー:テキスト内容を格納する 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++オンラインコンパイラ