命名空间
变体
操作

三/五/零法则

来自 cppreference.com
< cpp‎ | 语言
 
 
C++ 语言
一般主题
流程控制
条件执行语句
if
迭代语句(循环)
for
范围-for (C++11)
跳转语句
函数
函数声明
Lambda 函数表达式
inline 指定符
动态异常规范 (直到 C++17*)
noexcept 指定符 (C++11)
异常
命名空间
类型
指定符
const/volatile
decltype (C++11)
auto (C++11)
constexpr (C++11)
consteval (C++20)
constinit (C++20)
存储持续时间指定符
初始化
表达式
替代表示
字面量
布尔型 - 整型 - 浮点型
字符型 - 字符串型 - nullptr (C++11)
用户自定义 (C++11)
实用工具
属性 (C++11)
类型
typedef 声明
类型别名声明 (C++11)
转换
内存分配
特定于类的函数属性
explicit (C++11)
static

特殊成员函数
模板
其他
 

内容

[编辑] 三法则

如果一个类需要用户定义的 析构函数、用户定义的 复制构造函数 或用户定义的 复制赋值运算符,那么它几乎肯定需要这三个。

由于 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) {}
};

当基类旨在用于多态使用时,其析构函数可能必须声明为 publicvirtual。这会阻止隐式移动(并弃用隐式复制),因此特殊成员函数必须定义为 = 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 08/15/2012
  2. "关于零法则的一个担忧", Scott Meyers, 3/13/2014.