Left value, Right value and Move Semantics
右值引用
虽然c++11对value catagories进行了新划分,但最初c++沿用了c对于一个表达式的分类,只有左值(lvalue)和右值(rvalue),能获取内存地址的就是左值,不能的就是右值。对于左值的引用就是左值引用,对于右值的引用就是右值引用。
1 2 3
| int val = 6; int& lref = a; int&& rref = 6;
|
严格来讲,左值引用不能引用右值,右值引用不能引用左值,但是添加了const限定的左值引用可以引用右值:
拷贝构造
假设我们现在有一个简单的对象,对象里有一个整型数组和一个记录数组大小的变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class Array { private: size_t size_; int* data_; public: explicit Array(const size_t size): size_(size), data_(new int[size_]) {} ~Array() { std::cout << "deconstructing" << std::endl; if (data != nullptr) { delete[] data_; size_ = 0; } } Array(const Array& other): size_(other.size_), data_(new int[size_]) { std::cout << "deep copying" << std::endl; std::copy(other.data_, other.data_ + size_, data_); } };
|
在第二个拷贝函数中,我们传入了另一个Array对象并试图把该Array对象中的数据拷贝进入当前构造的对象当中去。很显然,这个传入的Array是一个左值,引用便是左值引用,同时构造函数中需要深拷贝。但假若我需要拷贝一个很大很复杂的对象,深拷贝的代价就会非常大。显然,我们需要一个更好的方法,比如是否可以直接把传入的Array对象里的数据直接拿过来,而非拷过来?
移动构造
这个时候右值引用就派上用场了。因为右值没有地址,可以直接复制给左值,那么只要传入一个右值在进行赋值就可以避免深拷贝了。
1 2 3 4 5 6
| Array(Array&& other): size_(other.size_), data_(other.data_) { std::cout << "move constructor" << std::endl; arr.data_ = nullptr; arr.size_ = 0; }
|
这时我们只要在构造中传入一个右值,就可以进行内存所有权的转移,从而避免进行深拷贝,进而减少性能消耗。同时,在移动构造中一定要释放源对象的指针,防止析构重复释放内存。
赋值运算符
赋值运算符也可以用移动语义来优化。
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
| Array& operator=(const Array& other) { std::cout << "Copy operator" << std::endl;
if (this != &other) { delete[] data_; size_ = other.size_; data_ = new int[size_]; std::copy(other.data_, other.data_ + size_, data_); } return *this; }
Array& operator=(Array&& other) { std::cout << "Move operator" << std::endl;
if (this != &other) { delete[] data_; size_ = other.size_; data_ = other.data_; other.data_ = nullptr; other.size_ = 0; } return *this; }
|
std::move()
还有一个问题没有解决,就是如何传入一个右值。c++在std中提供了一个move函数,这个函数虽然叫move,但它的功能非常简单,就是单纯的把一个左值变为右值。说白了,就是对static_cast<T&&>进行的封装。
此处A是一个左值,我们希望调用移动构造器,便使用move将A变为右值并传入。传入后A的内存数据被直接转移给了B,A中的内容被置空,等待析构。
此外,std::move()一般不用于基本类型,基本类型没有必要进行内存移动,移动速度不一定比拷贝快。
Reference
Understanding lvalues and rvalues in C and C++
What is move semantics?
Move Constructors and Move Assignment Operators (C++)