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) {}
// デストラクタ、コピー関数、ムーブ関数は不要!
};これらのルールに従うことで、一部の操作は定義されているのに他の操作が欠けている場合に発生する、二重削除、メモリリーク、ダングリングポインターなどのバグを防げます。
チャレンジ
簡単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()メソッド
- private メンバー:テキスト内容を格納する
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;
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
3コンストラクタとデストラクタ
デフォルトコンストラクタパラメータ付きコンストラクタコピーコンストラクタムーブコンストラクタコンストラクタ初期化リスト委譲コンストラクタデストラクタ徹底解説Rule of Three / Five / Zero復習 - Stringクラス6演算子オーバーロード
演算子オーバーロード入門算術演算子のオーバーロード比較演算子のオーバーロードストリーム演算子代入演算子のオーバーロード[] と () の演算子オーバーロード型変換演算子復習 - Matrixクラス自分で練習してみよう: C++オンラインコンパイラ