三/五/零之法则

< cpp‎ | language
 
 
C++ 语言
 

三之法则

若某个类需要用户定义的析构函数、用户定义的复制构造函数或用户定义的复制赋值运算符,则它几乎肯定三者全部都需要。

因为 C++ 在各种场合(按值传递/返回、操纵容器等)对对象进行复制和复制赋值,若可访问,这些特殊成员函数就会被调用,而且若用户不定义他们,则编译器会隐式定义。

如果类对某种资源进行管理,而资源句柄是非类类型的对象(裸指针、POSIX 文件描述符等),则这些隐式定义的成员函数通常都不正确,其析构函数不做任何事,而复制构造函数/复制赋值运算符则进行“浅复制”(复制句柄的值,而不复制底层资源)。

class rule_of_three
{
    char* cstring; // 以裸指针为动态分配内存的句柄
 
    void init(const char* s)
    {
        std::size_t n = std::strlen(s) + 1;
        cstring = new char[n];
        std::memcpy(cstring, s, n); // 填充
    }
 public:
    rule_of_three(const char* s = "") { init(s); }
 
    ~rule_of_three()
    {
        delete[] cstring;  // 解分配
    }
 
    rule_of_three(const rule_of_three& other) // 复制构造函数
    { 
        init(other.cstring);
    }
 
    rule_of_three& operator=(const rule_of_three& other) // 复制赋值
    {
        if(this != &other) {
            delete[] cstring;  // 解分配
            init(other.cstring);
        }
        return *this;
    }
};

通过可复制句柄来管理不可复制资源的类,可能必须将其复制赋值和复制构造函数声明为私有的并且不提供其定义,或将它们定义为弃置的。这是三之法则的另一种应用:删除其中之一而遗留其他被隐式定义很可能会导致错误。

五之法则

因为用户定义析构函数、复制构造函数或复制赋值运算符的存在阻止移动构造函数移动赋值运算符的隐式定义,所以任何想要移动语义的类必须声明全部五个特殊成员函数:

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:多态类应该抑制复制操作),这带来了下列的五之法则的通用说法:

C.21:若有任何默认操作被定义为 =delete,则应当对它们全部进行定义或 =delete

引用

  1. “零之法则”,R. Martinho Fernandes 8/15/2012
  2. “关于零之法则的考虑”,Scott Meyers, 3/13/2014