3/5/0のルール
3のルール
クラスがユーザ定義のデストラクタ、ユーザ定義のコピーコンストラクタ、またはユーザ定義のコピー代入演算子を必要とする場合は、ほとんど確実に3つすべてが必要となります。
C++ は、様々な状況において (引数、戻り値、コンテナ操作など) ユーザ定義型のオブジェクトをコピーおよびコピー代入するため、これらの特別なメンバ関数が (アクセス可能であれば) 呼ばれ、ユーザ定義されていなければ、それらはコンパイラによって暗黙に定義されます。
非クラス型のオブジェクトをハンドルとするリソース (生のポインタや POSIX のファイル記述子など) をクラスが管理している場合、暗黙に定義された特別なメンバ関数 (デストラクタは何も行わず、コピーコンストラクタおよびコピー代入演算子は「シャローコピー」を行う (ベースとなるリソースを複製することなくハンドルの値をコピーする)) は、一般的には、正しくありません。
class rule_of_three
{
char* cstring; // 動的に確保されたメモリブロックへのハンドルとして使用される生のポインタ
void init(const char* s)
{
std::size_t n = std::strlen(s) + 1;
cstring = new char[n];
std::memcpy(cstring, s, n); // データ投入
}
public:
rule_of_three(const char* s = "") { init(s); }
~rule_of_three()
{
delete[] cstring; // 解放
}
rule_of_three(const rule_of_three& other) // コピーコンストラクタ
{
init(other.cstring);
}
rule_of_three& operator=(const rule_of_three& other) // コピー代入
{
if(this != &other) {
delete[] cstring; // 解放
init(other.cstring);
}
return *this;
}
};
コピー可能なハンドルを用いてコピー不可能なリソースを管理するクラスは、コピー代入およびコピーコンストラクタを private で宣言し、定義を提供しないか削除されたものとして定義する必要があるかもしれません。 これは3のルールが適用される別のケースです。 ひとつを削除して残りを暗黙に定義させることはほとんど確実に間違った結果をもたらします。
5のルール
ユーザ定義のデストラクタ、コピーコンストラクタ、またはコピー代入演算子の存在は、ムーブコンストラクタおよびムーブ代入演算子の暗黙の定義を抑制するため、ムーブセマンティクスを望むあらゆるクラスは、5つの特別なメンバ関数をすべて宣言する必要があります。
class rule_of_five
{
char* cstring; // 動的に確保されたメモリブロックへのハンドルとして使用される生のポインタ
public:
rule_of_five(const char* s = "")
: cstring(nullptr)
{
if (s) {
std::size_t n = std::strlen(s) + 1;
cstring = new char[n]; // 確保
std::memcpy(cstring, s, n); // データ投入
}
}
~rule_of_five()
{
delete[] cstring; // 解放
}
rule_of_five(const rule_of_five& other) // コピーコンストラクタ
: rule_of_five(other.cstring)
{}
rule_of_five(rule_of_five&& other) noexcept // ムーブコンストラクタ
: cstring(std::exchange(other.cstring, nullptr))
{}
rule_of_five& operator=(const rule_of_five& other) // コピー代入
{
return *this = rule_of_five(other);
}
rule_of_five& operator=(rule_of_five&& other) noexcept // ムーブ代入
{
std::swap(cstring, other.cstring);
return *this;
}
// あるいは、両方の代入演算子を以下に置き換えることもできます。
// rule_of_five& operator=(rule_of_five other) noexcept
// {
// std::swap(cstring, other.cstring);
// return *this;
// }
};
3のルールと異なり、ムーブコンストラクタおよびムーブ代入演算子を提供しないことは、通常、間違いではありませんが、最適化の機会を逃すことになります。
0のルール
カスタムなデストラクタ、コピー/ムーブコンストラクタ、またはコピー/ムーブ代入演算子を持つクラスは、 (単一責任の原則により) 所有権を排他的に管理するべきです。 それ以外のクラスはカスタムなデストラクタ、コピー/ムーブコンストラクタ、またはコピー/ムーブ代入演算子を持つべきではありません。[1]
このルールは C++ Core Guidelines でも C.20: If you can avoid defining default operations, do (デフォルト操作の定義を回避できる場合は、しなさい) として述べられています。
class rule_of_zero
{
std::string cppstring;
public:
rule_of_zero(const std::string& arg) : cppstring(arg) {}
};
基底クラスが多態的な使用を意図している場合、そのデストラクタは public かつ virtual として定義される必要があるかもしれません。 これは暗黙のムーブを妨げる (そして暗黙のコピーを非推奨化する) ため、特別なメンバ関数はデフォルト化して宣言する必要があります。[2]
class base_of_five_defaults
{
public:
base_of_five_defaults(const base_of_five_defaults&) = default;
base_of_five_defaults(base_of_five_defaults&&) = default;
base_of_five_defaults& operator=(const base_of_five_defaults&) = default;
base_of_five_defaults& operator=(base_of_five_defaults&&) = default;
virtual ~base_of_five_defaults() = default;
};
しかし、これはクラスをスライシングされやすくします。 多相クラスがしばしばコピーを削除されたものとして定義する理由はこれです (C++ Core Guidelines の C.67: A polymorphic class should suppress copying (多相クラスはコピーを抑制するべき) を参照してください)。 これにより5のルールに対する一般的な表現 C.21: If you define or =delete any default operation, define or =delete them all (デフォルト操作のいずれかを定義または =delete する場合は、それらすべてを定義または =delete しなさい) が導かれます。