Rules of C++

Rules of C++

如果一个类定义没有显式申明一个复制构造函数,一个非显式的函数会隐式地定义。
如果该类定义申明了一个移动构造函数或者移动赋值运算符,隐式定义的复制构造函数被定义为以删除的;否则,其被定义为默认的。当该类有一个用户申明的复制赋值运算符或者析构函数,后者的情况被弃用。

If the class definition does not explicitly declare a copy constructor, a non-explicit one is declared implicitly. If the class definition declares a move constructor or move assignment operator, the implicitly declared copy constructor is defined as deleted; otherwise, it is defined as defaulted (8.4). The latter case is deprecated if the class has a user-declared copy assignment operator or a user-declared destructor.

三/五/零规则

三规则

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

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

若该类管理资源,而资源的柄是非类类型(生指针、 POSIX 文件描述符等),则隐式定义的特殊成员函数大体是错误的,其中析构函数无操作而复制构造函数/赋值运算符进行“浅复制”(复制柄的值,而不备份底下的资源)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class rule_of_three
{
char* cstring; // 用于指向动态分配内存块的句柄的生指针
public:
rule_of_three(const char* arg) : cstring(new char[std::strlen(arg)+1]) // 分配
{
std::strcpy(cstring, arg); // 复制内容
}
~rule_of_three()
{
delete[] cstring; // 解分配
}
rule_of_three(const rule_of_three& other) // 复制构造函数
{
cstring = new char[std::strlen(other.cstring) + 1];
std::strcpy(cstring, other.cstring);
}
rule_of_three& operator=(const rule_of_three& other) // 复制赋值
{
char* tmp_cstring = new char[std::strlen(other.cstring) + 1];
std::strcpy(tmp_cstring, other.cstring);
delete[] cstring;
cstring = tmp_cstring;
return *this;
}
// 另可重用析构函数和复制构造函数
// rule_of_three& operator=(rule_of_three other)
// {
// std::swap(cstring, other.cstring);
// return *this;
// }
};

通过可复制柄管理不可复制资源的类,必须声明复制赋值和复制构造函数为私有并不提供其定义,或定义它们为被删除。这是另一种三规则:删除一者并保留另一者为隐式定义很可能导致错误。

五规则

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class rule_of_five
{
char* cstring; // 用作指向动态分配内存块的句柄的生指针
public:
rule_of_five(const char* arg)
: cstring(new char[std::strlen(arg)+1]) // 分配
{
std::strcpy(cstring, arg); // 复制内容
}
~rule_of_five()
{
delete[] cstring; // 解分配
}
rule_of_five(const rule_of_five& other) // 复制构造函数
{
cstring = new char[std::strlen(other.cstring) + 1];
std::strcpy(cstring, other.cstring);
}
rule_of_five(rule_of_five&& other) : cstring(other.cstring) // 移动构造函数
{
other.cstring = nullptr;
}
rule_of_five& operator=(const rule_of_five& other) // 复制赋值
{
char* tmp_cstring = new char[std::strlen(other.cstring) + 1];
std::strcpy(tmp_cstring, other.cstring);
delete[] cstring;
cstring = tmp_cstring;
return *this;
}
rule_of_five& operator=(rule_of_five&& other) // 移动赋值
{
if(this!=&other) // 阻止自移动
{
delete[] cstring;
cstring = other.cstring;
other.cstring = nullptr;
}
return *this;
}
// 另可将二个赋值运算符以下面的替换
// rule_of_five& operator=(rule_of_five other)
// {
// std::swap(cstring, other.cstring);
// return *this;
// }
};

不同于三规则,不提供移动构造函数和移动赋值运算符通常不是错误,然而是对优化机会的错失。

零规则

拥有自定义析构函数、复制/移动构造函数或复制/移动赋值运算符的类应该排他地处理所有权(这遵循单一责任原则)。其他类不该有自定义析构函数、复制/移动构造函数或复制/移动赋值运算符。

1
2
3
4
5
6
class rule_of_zero
{
std::string cppstring;
public:
rule_of_zero(const std::string& arg) : cppstring(arg) {}
};

当基类为多态使用而设时,其析构函数可能必须声明为公开且为虚。这阻止隐式移动(并将隐式复制过时化),从而特殊成员函数必须声明为默认

1
2
3
4
5
6
7
8
9
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;
};

然而,若导出类不被动态分配,或仅在被存储于 std::shared_ptr 时动态分配(例如通过 std::make_shared ),则可避免它:共享指针调用导出类的析构函数,即使在被转型为 std::shared_ptr 后。

Reference