C++右值引用 完美转发
卖寂寞的小男孩 人气:0一、左值与右值
顾名思义,左值就是只能放在等号左边的值,右值是只能放在等号右边的值。
在C++Prime一书中,对左值和右值的划分为,左值是一个表示数据的表达式,右值是一个即将销毁的值(通常称为将亡值)。比如我们定义的一个变量就是一个左值,而字面常量,表达式返回值,传值返回函数的返回值就是右值。
10;//右值 int a = 10;//a是左值 add(2, 3);//右值 x+y;//右值 const int a;//左值
注意,const类型的变量是不能放在等号左侧来为它赋值的,但是他是一个左值。
这里给出一个区分两者的方式:可以取地址的就是左值,不能取地址的就是右值!
二、左值引用与右值引用
我们之前所写的引用都是左值引用符号是&,左值引用的底层是使用指针,它的作用是为对象取一个别名。
而右值引用就是给右值取别名,它的符号是&&,右值引用开辟了空间,得到的一个对象是左值。
int a = 10; int& d = a;//左值引用 int&& e = 10;//右值引用 int&& f = a + 1; int&& c = add(2, 3);
左值引用不能给右值取别名,右值引用也不能给左值取别名。但是如果对左值进行move(),对左值引用加上const是可以这样进行的。
move的意思就是保证除了赋值和销毁之外,不再使用该左值,即将a的属性转移到了e中,对左值move后是一共右值。
int&& c = a;//右值引用不能给左值取别名 int& d = add(3, 4);//左值引用不能给右值取别名 int&& e = move(a);//当对左值加move的时候可以 const int& f = add(3, 4);//当对引用加const后可以取别名
同时右值引用不像左值引用一样具有传递性:
int&& a = 10; a=20; cout<<&a<<endl; //int&& b = a;//错误
这是因为a是一个左值,我们可以打印a的地址,右值经过引用后得到的对象是一个左值。因此我们是可以对a进行赋值的。
三、右值引用应用
1.移动构造与移动赋值
移动构造与移动赋值在C++11中已经加入了STL容器的函数中:
string(string&& str) //移动构造
string& operator=(string&& str)//移动赋值
移动构造与移动赋值都是向函数中传入右值引用,它们的本质与右值基本相同,就是将一个将亡值的数据转移给另一个值。
我们可以在函数string中模拟实现一下移动构造和移动赋值,它们的本质就是调用swap函数完成赋值,而不是使用strcpy创建一个新对象。
1.模拟实现的string
为了方便观察,我们使用自己模拟实现的string来进行说明:
namespace my_string { class string { public: typedef char* iterator; iterator begin() { return _str; } iterator end() { return _str + _size; } //构造函数 string(const char* str = "") :_size(strlen(str)) , _capacity(_size) { cout << "string(char* str)" << endl; _str = new char[_capacity + 1]; strcpy(_str, str); } // s1.swap(s2) void swap(string& s) { ::swap(_str, s._str); ::swap(_size, s._size); ::swap(_capacity, s._capacity); } // 拷贝构造 string(const string& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(const string& s) -- 深拷贝" << endl; string tmp(s._str); swap(tmp); } // 移动构造 string(string&& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(string&& s) -- 资源转移" << endl; this->swap(s); } // 移动赋值 string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 转移资源" << endl; swap(s); return *this; } //赋值 string& operator=(const string& s) { cout << "string& operator=(string s) -- 深拷贝" << endl; string tmp(s); swap(tmp); return *this; } ~string() { delete[] _str; _str = nullptr; } //下标访问 char& operator[](size_t pos) { assert(pos < _size); return _str[pos]; } //调换顺序 void reserve(size_t n) { if (n > _capacity) { char* tmp = new char[n + 1]; strcpy(tmp, _str); delete[] _str; _str = tmp; _capacity = n; } } //插入 void push_back(char ch) { if (_size >= _capacity) { size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2; reserve(newcapacity); } _str[_size] = ch; ++_size; _str[_size] = '\0'; } //string operator+=(char ch) string& operator+=(char ch) { push_back(ch); return *this; } string operator+(char ch) { string tmp(*this); push_back(ch); return tmp; } const char* c_str() const { return _str; } private: char* _str; size_t _size; size_t _capacity; // 不包含最后做标识的\0 }; my_string::string to_string(int value) { my_string::string str; while (value) { int val = value % 10; str += ('0' + val); value /= 10; } reverse(str.begin(), str.end()); return str; } }
2.移动构造
当我们调用to_string的时候:
my_string::string ret = my_string::to_string(1234);
当我们不添加移动构造的时候,可以发现最终进行的是一次深拷贝和一次浅拷贝:
这里发现只调用了一次拷贝构造,这是因为编译器做了优化,如果不优化的话,str拷贝构造临时对象,然后临时对象作为to_string的返回值再拷贝构造给ret。其实是发生了两次拷贝构造。
但是编译器做了优化之后,在to_string函数快结束时,返回前直接用str构造ret。
当我们加入拷贝构造之后,会发现只发生了一次移动构造就可以了:
其实在这一过程中编译器也做了优化,str先拷贝构造形成一个临时对象,再由临时对象进行移动构造赋值给ret。
编译器做了优化之后,将str直接当成左值(相当于move了一下),然后进行移动构造生成ret。
通过观察打印结果可以发现,显然移动构造没有再开辟空间,而是直接将数据进行转移,节省了空间,由临时变量进行拷贝构造给ret还会创建一个新的对象,消耗空间。
3.移动赋值
my_string::string ret; ret = my_string::to_string(1234);//调用移动赋值
当不使用移动赋值的时候,以上代码是两段深拷贝实现的:
首先str会调用移动构造,生成临时对象,然后临时对象再调用赋值拷贝构造(深拷贝),定义ret。
当引入移动赋值之后,这个过程就变成了str调用移动构造生成临时对象,临时对象再通过移动运算符重载生成ret,整个过程中没有一次深拷贝。
C++11中,所有STL容器中,都会提供一个右值引用的版本。
四、默认移动构造和移动赋值重载函数
与六大成员函数一样,编译器在一定的条件下,也会生成自己的默认移动构造函数,只不过生成的条件更加复杂:
1.如果你自己没有实现移动构造函数,并且没有实现析构函数,拷贝构造,拷贝赋值构造中的任意一个。那么编译器会自动生成一个默认构造函数。默认生成的移动构造函数,对内置类型进行直接拷贝,对于自定义类型,如果有对应的移动构造函数就调用其对应的移动构造函数,如果没有那么调用拷贝构造。
2.如果你没自己实现移动赋值重载函数,且没有实现析构函数,拷贝构造,拷贝赋值重载中的任何一个,编译器会自动生成一个移动赋值重载函数。默认生成的移动赋值重载函数,对内置类型直接进行赋值,对于自定义类型,如果有对应的移动赋值重载函数就调用其对应的移动赋值重载函数,如果没有则调用拷贝赋值。
3.如果你提供了移动赋值构造或者移动赋值重载函数,那么编译器就不会自动生成。
五、完美转发
1.万能引用
在模板中,&&表示的不是右值引用,而是万能引用,即既可以接收左值,又可以接收右值。
void PerfectForward(T&& t) { Fun(forward<T>(t)); }
此时传入的t既可以是左值,也可以是右值。
2.完美转发
运行以下程序,发现最终识别的都是左值引用。
void Func(int&& x) { cout << "rvalue" << endl; } void Func(int& x) { cout << "lvalue" << endl; } template<class T> void PerfectForward(T&& t) { Func(t); } int main() { PerfectForward(10);//左值 int a; PerfectForward(a);//左值 PerfectForward(move(a));//左值 }
这是因为右值引用一旦引用了,就变成了左值,如果我们还希望保持该右值引用的特性的话,需要使用forward函数来对其进行封装:
Func(forward<T>(t));
forward(t)来进行封装的意义在于,保持t原来的属性,如果它原来是左值那么封装之后还是左值,如果它是右值的引用,则将其还原成右值。该函数的作用称为完美转发,由于这一性质,STL容器的插入也可以使用右值引用来实现。
即支持:
vector<int> v;v.push_back(111);
在该右值引用版本的插入中,调用的就是forward(val)。
加载全部内容