三/五/零之法则
三之法则
如果某个类需要用户定义的析构函数、用户定义的复制构造函数或用户定义的复制赋值运算符,那么它几乎肯定需要全部三者。
因为 C++ 在各种场合(按值传递/返回、操纵容器等)对对象进行复制和复制赋值时会在这些特殊成员函数可以访问的情况下调用它们,而且如果用户没有定义他们,那么编译器就会隐式定义。
如果类对某种资源进行管理,而资源句柄是非类类型的对象(裸指针、POSIX 文件描述符等),那么这些隐式定义的成员函数通常都不正确,该类的析构函数不会做任何事,而复制构造函数/复制赋值运算符会进行“浅复制”(复制句柄的值,而不复制底层资源)。
class rule_of_three { char* cstring; // 以裸指针为动态分配内存的句柄 rule_of_three(const char* s, std::size_t n) // 避免重复计数 : cstring(new char[n]) // 分配 { std::memcpy(cstring, s, n); // 填充 } public: rule_of_three(const char* s = "") : rule_of_three(s, std::strlen(s) + 1) {} ~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. 复制赋值 { if (this == &other) return *this; std::size_t n{std::strlen(other.cstring) + 1}; char* new_cstring = new char[n]; // 分配 std::memcpy(new_cstring, other.cstring, n); // 填充 delete[] cstring; // 解分配 cstring = new_cstring; return *this; } operator const char *() const // 访问器 { return cstring; } };
通过可复制句柄来管理不可复制资源的类,可能必须将它的复制赋值和复制构造函数声明为私有的并且不提供它们的定义,或将它们定义为弃置的。这是三之法则的另一种应用:只删除其一却任由其他特殊成员被隐式定义很可能会导致错误。
五之法则
因为用户定义的析构函数、复制构造函数或复制赋值运算符的存在会阻止移动构造函数和移动赋值运算符的隐式定义,所以任何想要移动语义的类必须声明全部五个特殊成员函数:
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; // } };
与三之法则不同的是,不提供移动构造函数和移动赋值运算符通常不是错误,但会导致失去优化机会。
零之法则
有自定义析构函数、复制/移动构造函数或复制/移动赋值运算符的类应该专门处理所有权(这遵循单一责任原则)。其他类都不应该拥有自定义的析构函数、复制/移动构造函数或复制/移动赋值运算符[1]
这条法则也在 C++ 核心指南(C++ Core Guidelines)中出现—— C.20:一旦可以避免定义默认操作就应当施行。
class rule_of_zero { std::string cppstring; public: rule_of_zero(const std::string& arg) : cppstring(arg) {} };
当有意将某个基类用于多态用途时,可能需要将它的析构函数声明为公开的虚函数。由于这会阻拦隐式移动(并弃用隐式复制)的生成,因而必须将各特殊成员函数声明为预置的[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++ 核心指南中的 C.67:多态类应该抑制复制操作),这带来了下列的五之法则的通用说法: