三/五/零法則
目錄 |
[編輯] 三法則
如果一個類需要使用者定義的解構函式、使用者定義的複製建構函式或使用者定義的複製賦值運算子,那麼它幾乎肯定需要所有這三者。
因為 C++ 在各種情況下會複製和複製賦值使用者定義型別的物件(按值傳遞/返回、操作容器等),所以這些特殊成員函式,如果可訪問,將被呼叫,如果它們不是使用者定義的,則由編譯器隱式定義。
如果類管理的資源控制代碼是非類型別物件(原始指標、POSIX 檔案描述符等),並且其解構函式不執行任何操作,複製建構函式/賦值運算子執行“淺複製”(複製控制代碼的值,而不復制底層資源),則不應使用隱式定義的特殊成員函式。
#include <cstddef> #include <cstring> #include <iostream> #include <utility> class rule_of_three { char* cstring; // raw pointer used as a handle to a // dynamically-allocated memory block public: explicit rule_of_three(const char* s = "") : cstring(nullptr) { if (s) { cstring = new char[std::strlen(s) + 1]; // allocate std::strcpy(cstring, s); // populate } } ~rule_of_three() // I. destructor { delete[] cstring; // deallocate } rule_of_three(const rule_of_three& other) // II. copy constructor : rule_of_three(other.cstring) {} rule_of_three& operator=(const rule_of_three& other) // III. copy assignment { // implemented through copy-and-swap for brevity // note that this prevents potential storage reuse rule_of_three temp(other); std::swap(cstring, temp.cstring); return *this; } const char* c_str() const // accessor { return cstring; } }; int main() { rule_of_three o1{"abc"}; std::cout << o1.c_str() << ' '; auto o2{o1}; // II. uses copy constructor std::cout << o2.c_str() << ' '; rule_of_three o3("def"); std::cout << o3.c_str() << ' '; o3 = o2; // III. uses copy assignment std::cout << o3.c_str() << '\n'; } // I. all destructors are called here
輸出
abc abc def abc
透過可複製控制代碼管理不可複製資源的類可能需要將複製賦值和複製建構函式宣告為private並且不提供它們的定義(C++11 前)將複製賦值和複製建構函式定義為= delete(C++11 起)。這是三法則的另一個應用:刪除一個而讓另一個隱式定義通常是錯誤的。
[編輯] 五法則
因為使用者定義(包括宣告為= default或= delete)的解構函式、複製建構函式或複製賦值運算子的存在會阻止移動建構函式和移動賦值運算子的隱式定義,所以任何需要移動語義的類都必須宣告所有五個特殊成員函式。
class rule_of_five { char* cstring; // raw pointer used as a handle to a // dynamically-allocated memory block public: explicit rule_of_five(const char* s = "") : cstring(nullptr) { if (s) { cstring = new char[std::strlen(s) + 1]; // allocate std::strcpy(cstring, s); // populate } } ~rule_of_five() { delete[] cstring; // deallocate } rule_of_five(const rule_of_five& other) // copy constructor : rule_of_five(other.cstring) {} rule_of_five(rule_of_five&& other) noexcept // move constructor : cstring(std::exchange(other.cstring, nullptr)) {} rule_of_five& operator=(const rule_of_five& other) // copy assignment { // implemented as move-assignment from a temporary copy for brevity // note that this prevents potential storage reuse return *this = rule_of_five(other); } rule_of_five& operator=(rule_of_five&& other) noexcept // move assignment { std::swap(cstring, other.cstring); return *this; } // alternatively, replace both assignment operators with copy-and-swap // implementation, which also fails to reuse storage in copy-assignment. // rule_of_five& operator=(rule_of_five other) noexcept // { // std::swap(cstring, other.cstring); // return *this; // } };
與三法則不同,未能提供移動建構函式和移動賦值通常不是錯誤,而是錯失的最佳化機會。
[編輯] 零法則
具有自定義解構函式、複製/移動建構函式或複製/移動賦值運算子的類應專門處理所有權(這遵循單一職責原則)。其他類不應具有自定義解構函式、複製/移動建構函式或複製/移動賦值運算子[1]。
此規則也出現在 C++ 核心準則中,作為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++ 核心準則中的C.67:多型類應抑制公共複製/移動),這導致了五法則的以下通用措辭: