The rule of three/five/zero
目次 |
三つの法則
クラスがユーザー定義の destructor 、ユーザー定義の copy constructor 、またはユーザー定義の copy assignment operator を必要とする場合、ほぼ確実にこれら3つすべてを必要とします。
C++は様々な状況(値渡し/値返し、コンテナの操作など)でユーザー定義型のオブジェクトをコピーおよびコピー代入するため、これらの特殊メンバ関数は、アクセス可能であれば呼び出され、ユーザー定義されていない場合、コンパイラによって暗黙的に定義されます。
暗黙的に定義される特殊メンバ関数は、クラスが リソースを管理する 場合で、そのハンドルが非クラス型(生ポインタ、POSIXファイル記述子など)のオブジェクトであり、デストラクタが何も行わず、コピーコンストラクタ/代入演算子が「浅いコピー」(ハンドルの値をコピーするが、基盤となるリソースを複製しない)を実行する場合には使用すべきではありません。
#include <cstddef> #include <cstring> #include <iostream> #include <utility> class rule_of_three { char* cstring; // 動的に確保されたメモリブロックへのハンドルとして使用される生ポインタ public: explicit rule_of_three(const char* s = "") : cstring(nullptr) { if (s) { cstring = new char[std::strlen(s) + 1]; // メモリ確保 std::strcpy(cstring, s); // データ設定 } } ~rule_of_three() // I. デストラクタ { delete[] cstring; // メモリ解放 } rule_of_three(const rule_of_three& other) // II. コピーコンストラクタ : rule_of_three(other.cstring) {} rule_of_three& operator=(const rule_of_three& other) // III. コピー代入演算子 { // 簡潔さのためにコピー・アンド・スワップで実装 // これによりストレージの再利用可能性が失われることに注意 rule_of_three temp(other); std::swap(cstring, temp.cstring); return *this; } const char* c_str() const // アクセサ { return cstring; } }; int main() { rule_of_three o1{"abc"}; std::cout << o1.c_str() << ' '; auto o2{o1}; // II. コピーコンストラクタを使用 std::cout << o2.c_str() << ' '; rule_of_three o3("def"); std::cout << o3.c_str() << ' '; o3 = o2; // III. コピー代入演算子を使用 std::cout << o3.c_str() << '\n'; } // I. ここですべてのデストラクタが呼び出される
出力:
abc abc def abc
コピー可能なハンドルを通じてコピー不可のリソースを管理するクラスは、 コピー代入演算子とコピーコンストラクタを private で宣言し、その定義を提供しない (C++11まで) コピー代入演算子とコピーコンストラクタを = delete として定義する (C++11以降) 必要があるかもしれません。これはRule of Threeの別の応用例です:一方を削除し、他方を暗黙的に定義されたままにすることは、一般的に正しくありません。
ルール・オブ・ファイブ
ユーザー定義の( = default または = delete で宣言された)デストラクタ、コピーコンストラクタ、またはコピー代入演算子が存在すると、 move constructor と move assignment operator の暗黙的な定義が妨げられるため、ムーブセマンティクスが望ましいクラスは、以下の5つの特殊メンバ関数をすべて宣言する必要があります:
class rule_of_five { char* cstring; // 動的に確保されたメモリブロックへのハンドルとして使用される生ポインタ // dynamically-allocated memory block public: explicit rule_of_five(const char* s = "") : cstring(nullptr) { if (s) { cstring = new char[std::strlen(s) + 1]; // メモリ確保 std::strcpy(cstring, s); // データ設定 { } ~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; // } };
Rule of Threeとは異なり、ムーブコンストラクタとムーブ代入演算子を提供しないことは、通常はエラーではなく、最適化の機会を逃していることになります。
ルール・オブ・ゼロ
カスタムデストラクタ、コピー/ムーブコンストラクタ、またはコピー/ムーブ代入演算子を持つクラスは、所有権の管理に専念すべきです(これは 単一責任原則 に従います)。他のクラスはカスタムデストラクタ、コピー/ムーブコンストラクタ、またはコピー/ムーブ代入演算子を持つべきではありません [1] 。
このルールはC++ Core Guidelinesにも C.20: デフォルト操作の定義を避けられる場合は避けること として記載されています。
class rule_of_zero { std::string cppstring; public: rule_of_zero(const std::string& arg) : cppstring(arg) {} };
基底クラスが多態的に使用されることを意図している場合、そのデストラクタは public かつ virtual として宣言する必要があるかもしれません。これにより暗黙的なムーブがブロックされ(また暗黙的なコピーは非推奨となり)、そのため特殊メンバ関数は = default [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; };
しかし、これによりクラスはスライシングを受けやすくなるため、ポリモーフィックなクラスはしばしばコピーを = delete と定義します(C++ Core Guidelinesの C.67: ポリモーフィックなクラスは公開コピー/ムーブを抑制すべき を参照)。これにより、以下のようなRule of Fiveの一般的な記述が導かれます: