Namespaces
Variants

The rule of three/five/zero

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

目录

三法则

如果一个类需要用户定义的 析构函数 、用户定义的 拷贝构造函数 ,或用户定义的 拷贝赋值运算符 ,那么它几乎必然需要全部三者。

由于 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 起) 。这是三法则的另一种应用场景:删除其中一个而让另一个隐式定义通常是不正确的。

五法则

由于用户自定义(包括声明为 = default = delete )的析构函数、拷贝构造函数或拷贝赋值运算符会阻止隐式生成 移动构造函数 移动赋值运算符 ,任何需要实现移动语义的类都必须显式声明全部五个特殊成员函数:

class rule_of_five
{
    char* cstring; // 原始指针,用作动态分配内存块的句柄
                   // 动态分配的内存块
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;
//  }
};

与三法则不同,未能提供移动构造函数和移动赋值运算符通常不算是错误,而是错失了优化机会。

零法则

具有自定义析构函数、复制/移动构造函数或复制/移动赋值运算符的类应当专门处理所有权问题(这遵循 单一职责原则 )。其他类不应具有自定义析构函数、复制/移动构造函数或复制/移动赋值运算符 [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:多态类应禁止公开的拷贝/移动操作 ),从而形成以下五法则的通用表述:

C.21: 如果定义或=delete任何拷贝、移动或析构函数,请定义或=delete全部相关函数。

外部链接

  1. "零法则", R. Martinho Fernandes 2012年8月15日
  2. "关于零法则的思考", Scott Meyers, 2014年3月13日 .