三/五/零法则
内容 |
[编辑] 三法则
如果一个类需要用户定义的 析构函数、用户定义的 复制构造函数 或用户定义的 复制赋值运算符,那么它几乎肯定需要这三个。
由于 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: rule_of_three(const char* s, std::size_t n) : cstring(new char[n + 1]) // allocate { std::memcpy(cstring, s, n); // populate cstring[n] = '\0'; // tail 0 } explicit rule_of_three(const char* s = "") : rule_of_three(s, std::strlen(s)) { } ~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 { if (this == &other) return *this; rule_of_three temp(other); // use the copy constructor std::swap(cstring, temp.cstring); // exchange the underlying resource 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 声明)析构函数、复制构造函数或复制赋值运算符的存在会阻止 移动构造函数 和 移动赋值运算符 的隐式定义,任何需要移动语义的类都必须声明所有五个特殊成员函数。
#include <cstddef> #include <cstring> #include <utility> class rule_of_five { char* cstring; // raw pointer used as a handle to a // dynamically-allocated memory block public: rule_of_five(const char* s, std::size_t n) : cstring(new char[n + 1]) // allocate { std::memcpy(cstring, s, n); // populate cstring[n] = '\0'; // tail 0 } explicit rule_of_five(const char* s) : rule_of_five(s, std::strlen(s)) { } ~rule_of_five() // I. destructor { delete[] cstring; // deallocate } rule_of_five(const rule_of_five& other) // II. copy constructor : rule_of_five(other.cstring) { } rule_of_five& operator=(const rule_of_five& other) // III. copy assignment { if (this == &other) return *this; rule_of_five temp(other); // use the copy constructor std::swap(cstring, temp.cstring); // exchange the underlying resource return *this; } rule_of_five(rule_of_five&& other) noexcept // IV. move constructor : cstring(std::exchange(other.cstring, nullptr)) { } rule_of_five& operator=(rule_of_five&& other) noexcept // V. move assignment { rule_of_five temp(std::move(other)); std::swap(cstring, temp.cstring); return *this; } };
与三法则不同,没有提供移动构造函数和移动赋值运算符通常不是错误,但会导致性能下降。
[编辑] 零法则
具有自定义析构函数、复制/移动构造函数或复制/移动赋值运算符的类应该专门处理所有权(这来自 单一职责原则)。其他类不应具有自定义析构函数、复制/移动构造函数或复制/移动赋值运算符[1]。
此规则也出现在 C++ 核心准则中,为 C.20:如果你可以避免定义默认操作,请这样做。
class rule_of_zero { std::string cppstring; public: // redundant, implicitly defined is better // 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:多态类应该禁止公开复制/移动),这导致了以下关于五法则的通用措辞。