C++11线程指南(四)--右值引用与移动语义
1. 按值传递
什么是按值传递?当一个函数通过值的方式获取它的参数时,就会包含一个拷贝的动作。编译器知道如何去进行拷贝。如果参数是自定义类型,则我们还需要提供拷贝构造函数,或者赋值运算符来进行深拷贝。然而,拷贝是需要代价的。在我们使用STL容器时,就存在大量的拷贝代价。当按值传递参数时,会产生临时对象,浪费宝贵的CPU以及内存资源。
需要找到一个减少不必要拷贝的方法。移动语义就是其中一种。
2. 右值引用
此处介绍右值引用的目的,是为了实现后面的移动语义。右值引用使得我们可以分辨一个值是左值还是右值。C++11标准规定右值引用只能绑定右值,它使用两个&符号进行声明。
int&& rvalue_ref = 99;下面就是一个右值引用例子:
#include<iostream> void f(int& i) { std::cout<<"lvalue ref: "<<i<<std::endl; } void f(int&& i) { std::cout<<"rvalue ref: "<<i<<std::endl; } int main() { int i=99; f(i); //lvalue ref called f(99); //rvalue ref called f(std::move(i)); //rvalue ref called return 0; }运行结果为:
lvalue ref: 99
rvalue ref: 99
rvalue ref: 99
其中std::move函数的作用就是将一个左值转换为成右值。
如果一个表达式会生成一个临时对象,则它就是一个右值。如下:
#include<iostream> int getValue() { int i = 22; return i; } int main() { std::cout<<getValue()<<std::endl; return 0; }getValue()就是一个右值。注意:返回的这个值并不是i的引用,而是一个临时值。
在C++0x中,使用左值引用仅能绑定const类型的临时对象。
const int& val = getValue(); // OK int& val = getValue(); // Wrong!但是,C++11中的右值引用允许我们绑定一个mutable引用到rvalue,但不是lvalue。换言之,右值引用可以完美的判断一个值是否为临时对象。
const int&& val = getValue(); // OK int&& val = getValue(); // OK下面进行一下比较:
void printReference (const int& value) { std::cout << value; } void printReference (int&& value) { std::cout << value; }第一个函数的参数类型为const lvalue, 它可以接收任何传入参数,无论是rvalue还是lvalue. 但是,第二个函数只能接收右值引用。
换言之,我们可以使用函数重载,即一个使用左值引用参数,另一个使用右值引用参数,来判断传入的参数是左值还是右值。也就是说,C++11引入了一个新的类型, non-const reference, 即右值引用,声明方式为T&&。它代表一个初始化后还允许被修改的临时值。这也是移动语义的基础。
#include<iostream> void printRef(int& value) { std::cout<<"lvalue: value = "<<value<<std::endl; } void printRef(int&& value) { std::cout<<"rvalue: value = "<<value<<std::endl; } int getVal() { int tmp = 88; return tmp; } int main(){ int i = 11; printRef(i); printRef(getVal()); //printRef(88); return 0; }运行结果为:
lvalue: value = 11
rvalue: value = 88
注意:第一个printRef函数中,参数没有const修饰符,这样使得它只能接受左值。
到此为止,我们可以写出两个区别明显的重载函数:一个只接受左值参数,另一个只接受右值参数。有何好处呢?它给予了我们一种以更少代码实现更有效率程序的方法!
对右值引用的总结:
1)int&& a: C++11中的新类型-右值引用采用的声明方式
2)non-const 左值引用绑定到一个对象
3)右值引用绑定到一个通常不会再被使用的临时对象
3. 移动语义
在C++03中,如果参数按值传递,就会隐含一个不必要的深拷贝代价在里边。我们可以使用右值引用来避免深拷贝带来的性能损失。基于前面的论述,我们已经有了一种可以用来判断是临时对象还是永久对象的方法。现在的问题是,如何使用它呢?
右值引用的主要作用就是用来创建移动构造函数(move constructor)以及移动赋值运算符(move assignment operator)。移动构造函数,类似于拷贝构造函数,使用对象实例做为参数并且基于原始对象来创建一个新的实例。只是,移动构造函数可以避免内存重新分配,因为我们知道它提供了一个临时对象。
换言之,右值引用和移动语义避免了不必要的临时对象拷贝。我们无需拷贝临时对象,这样,临时对象所需的资源,可以使用于其它对象。
右值通常是临时的并且可以被修改的。如果我们知道函数参数是一个右值,可以把它当作临时存储使用,或者获取其内容,而不会改变程序的输出。这意味着我们可以移动其内容,而无需拷贝其内容。这样节省了大量的内容分配,并可对大量动态内存结构程序进行优化。
下面是一个典型的使用移动语义的类定义:
#include<iostream> #include<algorithm> #include<vector> class Dummy { public: explicit Dummy(size_t length):_length(length), _data(new int[length]) { std::cout<<"Dummy(size_t).length = "<<_length<<"."<<std::endl; } ~Dummy() { std::cout<<"~Dummy().length = "<<_length<<"."<<std::endl; if(_data!=NULL){ std::cout<<"delete resource."<<std::endl; delete _data; } } Dummy(const Dummy&other):_length(other._length),_data(new int[other._length]) { std::cout<<"Dummy(const &Dummy).length = "<<other._length<<".Copying resource."<<std::endl; std::copy(other._data, other._data+_length, _data); } //Copy assignment operator Dummy& operator=(const Dummy& other) { std::cout<<"operator=(const Dummy&).length = "<<other._length<<". Copying resource."<<std::endl; if(this != &other) { delete _data; _length = other._length; _data = new int[_length]; std::copy(other._data, other._data+_length, _data); } return *this; } //Move constructor Dummy(Dummy&& other):_length(0),_data(NULL) { std::cout<<"Dummy(Dummy&&).length = "<<other._length<<". Moving resource."<<std::endl; //copy the data pointer and its length from the source object _data = other._data; _length = other._length; //release the data pointer from the source object so that the destructor does not free //the memory multiple times. other._data = NULL; other._length = 0; } //Move assigment operator Dummy& operator=(Dummy&& other) { std::cout<<"operator=(Dummy&&).length = "<<other._length<<"."<<std::endl; if(this != &other) { delete _data; //copy the data pointer and its length from the source object _data = other._data; _length = other._length; //release the data pointer from the source object so that the destructor does not free //the memory multiple times other._data = NULL; other._length = 0; } return *this; } private: size_t _length; int* _data; }; int main() { std::vector<Dummy> vec; vec.push_back(Dummy(55)); vec.push_back(Dummy(77)); //insert a new element into the second position of the vector vec.insert(vec.begin()+1, Dummy(66)); return 0; }运行结果为:
Dummy(size_t).length = 55.
Dummy(Dummy&&).length = 55. Moving resource.
~Dummy().length = 0.
Dummy(size_t).length = 77.
Dummy(Dummy&&).length = 77. Moving resource.
Dummy(const &Dummy).length = 55.Copying resource.
~Dummy().length = 55.
delete resource.
~Dummy().length = 0.
Dummy(size_t).length = 66.
Dummy(Dummy&&).length = 66. Moving resource.
Dummy(const &Dummy).length = 55.Copying resource.
Dummy(const &Dummy).length = 77.Copying resource.
~Dummy().length = 55.
delete resource.
~Dummy().length = 77.
delete resource.
~Dummy().length = 0.
~Dummy().length = 55.
delete resource.
~Dummy().length = 66.
delete resource.
~Dummy().length = 77.
delete resource.
4. 移动构造函数
下面是一个最简单的移动构造函数:Dummy(Dummy&& other) noexcept // C++11 - specify non-exception throwing function { _date = other._data; // shallow copy other._date = nullptr; }注意:上面函数没有分配任何新的资源,只是将内容移动了而不是拷贝: other中的内容移到了一个新成员里边,然后other中的内容被清除了。它占用了other的资源并且将other设置为了默认构造函数时的状态。最重要的事实是没有内容的分配开销。我们只是分配了一个地址,只需很少的几个机器指令即可实现。
假设这个地址指向的是包含上万个整数的数组,我们无需拷贝其中的元素,无需创建新的东西,而只是移动了它们。如果使用旧的拷贝构造函数,且这个类有一个拥有上万个元素的成员数组,则我们需要大量的赋值操作,付出的代价很大。现在,有了移动构造函数之后,可以节省很多。
移动构造函数比拷贝构造函数快很多,因为它既不分配内存,也不拷贝内存块。
5. 移动赋值运算符
一个简单的移动赋值运算符如下:Dummy& operator=(Dummy&& other) noexcept { _data = other._data; other._data = nullptr; return *this; }移动赋值运算符与拷贝构造函数类似,除了转移source object之前,它会释放object所拥有的资源。步骤如下:
1). 释放*this所拥有的资源
2). 转移other的资源
3). 将other设置为默认状态
4). 返回*this
5. 结果分析
因为C++11支持右值引用,这样vector::push_back()函数相当于有两个版本:一个像以前一样接受左值参数const T&, 另外一个新的接受右值参数T&&。
main()函数中调用了两次push_back来插入到vector中:
std::vector<A> vec; vec.push_back(A(55)); vec.push_back(A(77));这两个push_back,实际上都使用的push_back(T&&), 因为传入的参数是右值。push_back(T&&)会将参数中的资源,移动到vector中的对象A, 使用的是A的移动构造函数。在C++03中,相同的这段代码会进行参数的赋值,因为会调用参数的拷贝构造函数。
如果传入的参数是一个左值,则push_back(const T&)会被调用:
std::vector<A> vec; A obj(25); // lvalue vec.push_back(obj); // push_back(const T&)不过,我们可以使用static_cast将左值引用转换为右值引用,使得调用的是push_back(T&&)。
// calls push_back(T&&) vec.push_back(static_cast<A&&>(obj));另外一种办法是,使用std::move()来实现:
// calls push_back(T&&) vec.push_back(std::move(obj));总结起来,push_back(T&&)看似总是最近选择,因为它减少了不必要的拷贝开销。然而,需要记住的是push_back(T&&)总会清空传入的参数。如果需要在执行一个push_back()操作后,仍然保持参数原始状态,则还是需要选择拷贝语义(拷贝构造函数),而不是移动语义。
6. 使用move()交换对象
下面例子显示如何使用move来交互对象#include<iostream> class Dummy { public: //constructor explicit Dummy(size_t length):_length(length),_data(new int[length]) { } //move constructor Dummy(Dummy&& other) { _data = other._data; _length = other._length; other._data = nullptr; other._length = 0; } //move assignment operator Dummy& operator= (Dummy&& other) noexcept { _data = other._data; _length = other._length; other._data = nullptr; other._length = 0; return *this; } void swap(Dummy& other) { Dummy tmp = std::move(other); other = std::move(*this); *this = std::move(tmp); } int getLength() { return _length; } int* getData() { return _data; } private: size_t _length; int* _data; }; int main() { Dummy a(11),b(22); std::cout<<a.getLength()<<" "<<b.getLength()<<std::endl; std::cout<<a.getData()<<" "<<b.getData()<<std::endl; a.swap(b); std::cout<<a.getLength()<<" "<<b.getLength()<<std::endl; std::cout<<a.getData()<<" "<<b.getData()<<std::endl; return 0; }运行结果为:
11 22
0x1a31010 0x1a31050
22 11
0x1a31050 0x1a31010
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。