三/五/零法则
目录 |
[edit] 三法则
如果一个类需要用户定义的析构函数、用户定义的复制构造函数或用户定义的复制赋值运算符,那么它几乎肯定需要全部三个。
因为 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 起)。这是三法则的另一个应用:删除一个而将另一个留给隐式定义通常是不正确的。
[edit] 五法则
由于用户定义的(包括声明为 = 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; // } };
与三法则不同,未能提供移动构造函数和移动赋值通常不是错误,而是一个错失的优化机会。
[edit] 零法则
具有自定义析构函数、复制/移动构造函数或复制/移动赋值运算符的类应专门处理所有权(这遵循单一职责原则)。其他类不应具有自定义析构函数、复制/移动构造函数或复制/移动赋值运算符[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:多态类应禁止公共复制/移动),这导致了五法则的以下通用措辞