I learned something today: C++ (part II)

Move semantics

​ 最近在读一本名叫“C++ High Performance”的书,觉得有一定帮助,学习了一点现代的C++,故记录下自己通俗的理解。

Rule of three

​ 前人的经验告诉我们,一个类应该负责好自己所拥有的资源的管理,当一个类被复制(copy),被赋值(assign)到其他类或是被析构(destruct)的时候,它应对所拥有的资源也应该做出相应的正确的操作。这一理念在实践中被提炼为 the rule of three。“three” 代表一个类的 copy-constructor,copy-assignment 和 destructor 三个成员函数,如果你自定义了这三个函数中的其中一个,那么你很有可能还需明确定义其他两个函数。如下面Buffer类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Buffer { 
public:
// other functions...
// Copy constructor
Buffer(const Buffer& other) : size_{other.size_} {
ptr_ = new float[size_];
std::copy(other.ptr_, other.ptr_ + size_, ptr_);
}
// Copy assignment
auto& operator=(const Buffer& other) {
auto tmp = new float[other.size_];
delete [] ptr_;
ptr_ = tmp;
size_ = other.size_;
std::copy(other.ptr_, other.ptr_ + size_, ptr_);
return *this;
}
// Destructor
~Buffer() {
delete [] ptr_;
ptr_ = nullptr;
}
private: size_t size_{0}; float* ptr_{nullptr};
}

​ 然而,这样有很多复制的操作,复制是有代价的,有时我们并不想要复制,传入的对象也不需要被保留,比如当我们把一个函数的返回值直接作为函数参数时。使用指针可以避免一些复制,但管理指针也并不那么方便:只能靠程序员自己。

Rule of five and Move semantics

​ 于是,在the rule of three 的基础上,有了 the rule of five 。为了解决复制带来的问题,我们定义两个全新的函数:move constructor 和move assignment operator 。从名字就可以猜到,这两个函数不复制,而是直接移动资源。一个类定义了自己的move constructor后,就可以实现move semantics。

​ 下面举一个利用move semantics带来好处的例子:假设我们要把若干个前面的buffer类,放入std::vector里。当std::vector容量不够的时候,必须重新分配更多的内存,然后利用拷贝构造函数,把原来内存里的旧对象一个个复制到新内存中,然后再摧毁旧的对象。但是如果我们的buffer类定义了move constructor ,std::vector就可以直接把旧对象移过来。

​ 那么事不宜迟,我们马上为buffer类加上move constructor 和move assignment operator :

1
2
3
4
5
6
7
8
9
10
11
12
Buffer(Buffer&& other) noexcept  //不使用noexcept将不便于STL使用move semantics
: ptr_{other.ptr_}, size_{other.size_} {
other.ptr_ = nullptr;
other.size_ = 0;
}
auto& operator=(Buffer&& other) noexcept {
ptr_ = other.ptr_;
size_ = other.size_;
other.ptr_ = nullptr;
other.size_ = 0;
return *this;
}

​ 可以看到,我们把拷贝构造函数里的 Buffer&改成了 Buffer&& :有或是没有&&,就代表了我们是想要“move”还是“copy”。&&这个东西用C++术语来说,叫做Rvalue Reference Declarator ,表示这两个函数接收所谓的r-value , 当我们调用构造函数,传入的是r-value时,我们便运用的是“move”而不是“copy”。

​ 那么什么是r-value?粗要的理解,r-value就是我们没给名字的对象。例如我们直接用函数的返回值做参数。同时也可以用 std::move() 得到r-value。但是注意,int,float等等原始的数据类型只会被拷贝。见如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using namespace std;
int main(){
int x = 1;
string s = "223";
auto y = move(x); //没有效果,相当于 auto y = x;
auto z = move(s);
cout<<x<<endl;
cout<<s<<endl; //‘s’ is moved
cout<<y<<endl;
cout<<z<<endl;
return 0;
}
/*输出:
1

1
223
*/