这篇文章的起因是我最近写Leetcode上的设计类题目比较多,因此对C++的
The Rule of Five
产生了兴趣,想以此来确定一个通用的代码模板,规范自己今后的所有自定义Class的书写。
The Rule of Three
The Rule of Three指的是一个Class的destructor, copy
constructor, copy assignment
operator。通常情况下,这三者是在一起出现的:也就是说,如果我们需要定义(define)其中的一个,那么往往意味着我们需要同时定义这三个member
function。对于这一情况,《C++ Primier》中有如下的描述:
Class that need destructor need copy and assignment
当我们的Class涉及内存资源管理时(一般是存在new动态分配内存的情况时),使用Compiler生成的(default关键字)三个member
function往往是错误的,因为这种自动生成的member
function只会对指针member进行浅拷贝,而不是将实际资源复制一份。
The Rule of Five
The Rule of Five指的是一个Class的destructor, copy
constructor, copy assignment
operator(这三者又被称为The Rule of Three), 外加 move
constructor, move assignment operator。这五个member
function又被称作为copy-control member。
如果我们显式地定义了(或者是= default,
= delete这样的声明)任何的一个copy-control
member,那么就会阻止Compiler为我们生成(synthesize)默认的move
constructor 和 move assignment
operator。如果我们希望获得性能上的优化,那么同样需要自己显式地定义这两个member
function。需要注意的是,与The Rule of Three不同,如果我们不提供move
constructor, move assignment
operator的定义,那么不会造成错误,只是会丧失优化的机会。
如果我们就是不提供move constructor和move assignment operator, Compiler也不进行自动生成,那么Compiler会把Rvalue reference当作一般的reference来看待,此时就会调用copy constructor和copy assignment operator来处理,极大可能会造成多余的内存空间申请和元素拷贝。
The Rule of Zero
如果我们的Class中所有的member都遵循了The Rule of Five时,那么这个Outside
Class就不需要再自定义任何的copy-control
member,相当于说完成了一种封装,最典型的例子如下:
class rule_of_zero { |
Template
下面给出标准的自定义Class的书写模版,以后凡是涉及The Rule of Five的情况都可以依照该模板来写:
class MyClass { |
对于这个模板,我想来着重解释一下几个细节:
首先是operator=:这里只是用一个MyClass& operator=(MyClass other)来代替copy
assignment
operatorMyClass& operator(const MyClass &other) 和
move assignment operator
MyClass& operator=(MyClass &&other)。由于传入的参数是值而非引用,因此会相应地隐式调用copy
constructor或move constructor,生成MyClass other
其次是friend void swap:这里我们自定义的swap函数不应该被视为MyClass中的一个member
function,而是定义在当前namespace中的一个独立函数。此时的friend函数是可以被ADL机制发现的。
A friend function defined inside a class is:
- placed in the enclosing namespace
- automatically
inline- able to refer to static members of the class without further qualification
然后是using std::swap:
引入标准库中的std::swap函数作为后备。对于没有namespace前缀的函数,C++在函数调用时遵循ADL(Argument-
dependent lookup)
的查找规则。我们这里只需要知道,ADL使得我们可以使用在函数实参(argument)类型的namespace中定义的同名函数(在这里就是我们member variable
class自己定义的friend void swap)。如果以上的查找都找不到名为swap的函数,那么最后使用我们引入的标准库中的std::swap。
最后是noexcept: 对于move constructor和move assignment
operator,最好是可以声明为noexcept,这主要是为了优化上的考量,例如std::vector<T>中的push_back函数,只有在保证T
有noexcept声明的move constructor,move assignment
operator时,push_back函数才会在执行时使用move操作来优化;如果不能保证noexcept,那么只能退化为调用copy
constructor, copy assignment operator的操作了。
Copy-and-Swap Idiom
在C++03时,对于std::swap,它使用的是copy
constructor和copy assignment operator, 其内部的实现可以近似等价于:
template<typename T> void swap(T& t1, T& t2) { |
在C++11以后,对于std::swap,它使用的是move
constructor和move assignment
operator,要求T必须是MoveConstructible和MoveAssignable的,其内部实现可以近似等价于:
template<typename T> void swap(T& t1, T& t2) { |
通过copy-and-swap的配合使用,可以有效地避免我们单独书写copy/move
assignment
operator时的两个问题:exception(必须保证如果new时出现了exception,this的值并没有改变)
和 self-assignment(手动判断&other == this的情况)
分别单独书写copy/move assignment operator的示例如下:
class MyClass { |
同样地,我们的move
constructor也借助了copy-and-swap的方法(这里必须保证使用的default
constructor不会造成任何内存资源申请,防止无意义的new操作)。如果不用copy-and-swap,单独书写move
constructor的示例如下:
class MyClass { |
References
- [CppReference] The rule of three/five/zero
- [Cpp Pattern] Copy-and-swap
- [StackOverflow] What is the copy-and-swap idiom?
- [StackOverflow] How does the standard library implement std::swap?
- [StackOverflow] public friend swap member function
- [StackOverflow] What is "Argument-Dependent Lookup" (aka ADL, or "Koenig Lookup")?
- [StackOverflow] How does "using std::swap" enable ADL?