人生在世天天天,日月如梭年年年,富贵之家有有有,贫困之人寒寒寒,升官发财得得得,两腿一蹬完完完。
1. 左值、右值
1.1 早期设定
首先我们来关注什么是左值右值。在上古时期,这个世界的引用本来是不分左值右值的,结果会出现这样的问题:
I made one serious mistake, though, by allowing a non-const reference to be initialized by a non-lvalue. For example:
void incr(int& rr) { rr++; }void g(){double ss = 1;incr(ss); // note: double passed, int expected// (fixed: error in Release 2.0)cout << ss << endl;}// output: 1.0
Because of the difference in type the int& cannot refer to the double passed so a temporary was generated to hold an int initialized by ss’s value. Thus, incr() modified the temporary, and the result wasn’t reflected back to the calling function.
这是说在上古时期上文的代码是合法的,具体的执行流程是:
ss是一个double类型的变量,所以会进行一个转化,把ss类型转换为一个int变量。- 这个转换相当于产生了一个匿名的变量所以按照现代人的理解这是一个 rval,按照上古语法
int &可以接受一个 rval。 - 所以这里的
rr++相当于是对一个临时变量进行操作,并不会对外面的ss产生影响。
这并不符合调用函数者的预期,所以后来 C++ 引入了左值和右值的区分,现在这段代码已经不能过编译了,具体提示信息如下:
error: cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type in incr(ss);
1.2 左值右值如何区分
判断左值和右值的方法有两种:
- 在等号左边的值就称为左值而在等号右边的称为右值
- 另外在c++中还有一种判别方法就是可以取地址,有名的就是左值;不能取地址、没有名的就是右值
对于左值我们用:typename &;对于右值我们用:typename &&。
1.3 左值右值的意义
引用本来的初衷可能是为了简化指针的使用,降低相关功能的语法难度。这个可以解释左值引用的出现。对于右值引用来说,我看来有两种意义:
- 实现一些语法功能。
- 在某些情况下提高性能。
关于第一点我们可以考虑这样一件事情:假设现在我们希望使用引用,于是我们写了一个函数:
void calc(int &val){//...}
那么在现代语法中这样的函数将无法接受常数作为它的参数,因为常变量是一个右值,而参数是一个左值引用。为了解决这个问题我们可以使用:
void calc(const int &val){//...}
在参数前面加上 `const`修饰就好了,然而这样的就限制了这个函数不能够修改`val`的值。所以我们可以这样:
void calc(int &&val){//...}
那这样的话如果我们想让一个左值作函数的参数呢?我们只需要使用`std::move()`函数即可,比如:
void calc(int &&val){//...}int a = 10;calc(std::move(a));
关于第二点,我们考虑这样一个情景:有时候我们调用一个函数返回了一个比较大的变量,我们后面会多次用到这个变量,那么我们也声明一个临时变量把函数返回值接住就可以了。然而这样会产生两次拷贝构造,第一次发生在函数返回的过程中,第二次发生在用临时变量把这个返回值接住的过程中。但是如果我们用一个右值引用来接就可以减少一次拷贝构造了:
struct node{int a[100];} tmpNode;tmpNode retNode(){tmpNode tmp;//...return tmp;}int main(){tmpNode tmp = retNode();// 发生两次拷贝构造tmpNode &&tmp2 = retNode();// 提高性能,减少一次拷贝构造// do sth. with tmp2}
从这个例子中可以看出,右值引用可以看成是一个本来要销毁的临时变量的生命延长。
1.4 左值右值引用本身的类型
引用本身也是一种变量,那么它们到底是左值还是右值呢?被声明出来的左、右值引用都是左值。因为被声明出的左右值引用是有地址的,也位于等号左边。
这里需要明确一个我自己容易搞混的概念:如果一个函数的某个参数是引用(不管是左值引用还是右值引用),并不意味着你传入的参数也是一个对应的引用,事实上你显然应该传入的是一个值。所以引用作为函数参数的时候和值作为函数参数还是有一定的区别的。我们看下面的代码:
// 形参是个右值引用void change(int&& right_value) {right_value = 8;}int main() {int a = 5; // a是个左值int &ref_a_left = a; // ref_a_left是个左值引用int &&ref_a_right = std::move(a); // ref_a_right是个右值引用change(a); // 编译不过,a是左值,change参数要求右值change(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值change(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值change(std::move(a)); // 编译通过change(std::move(ref_a_right)); // 编译通过change(std::move(ref_a_left)); // 编译通过change(5); // 当然可以直接接右值,编译通过cout << &a << ' ';cout << &ref_a_left << ' ';cout << &ref_a_right;// 打印这三个左值的地址,都是一样的}
可以看到,不管是左值引用还是右值引用,如果是被声明出来的,那么它们都是左值,不能传递给函数中的右值引用参数。
事实上,左值引用只可能是左值,右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值。或者说:作为函数返回值的 && 是右值,直接声明出来的 && 是左值。比如int &&ref = std::move(a)等号右侧就是右值。
2. std::move()
在上文我们已经多次用到这个函数,但是都没有介绍过它到底是什么意思。std::move并不能移动任何东西,它唯一的功能是将左值转化为右值,继而我们可以用右值引用引用这个值。很多文章说到这个东西可以提升性能,实际上它本身并不能提升任何性能,只是它在某些场景下可以减少拷贝和析构的次数。那么具体什么时候可以提升性能呢?
正如上文这段代码所示,使用右值引用有时候可以减少一次拷贝。类似的,我们还可能遇到这样的情况:我们的构造函数中的参数是一个函数的返回值,我们实际上并不需要深拷贝,因为之前函数的返回值反正也很快会过生命周期而销毁,所以我们不如把它内部的东西挪过来就好了,于是有:
class Array {public:......// 优雅Array(Array&& temp_array) {data_ = temp_array.data_;size_ = temp_array.size_;// 为防止temp_array析构时delete data,提前置空其data_temp_array.data_ = nullptr;}public:int *data_;int size_;};Array a;Array b(std::move(a));Array c(retArray());
我们把这个叫做:移动拷贝构造函数。
还有一些情形,比如我们在使用 STL 的时候想向容器中插入一些数据,比如我们想使用std::vector<int>的push_back()函数,但是数据都是临时产生的,我们根本不想把它们保留。如果直接插入的话实际上会发生深拷贝,浪费时间,于是我们使用std::move()就可以实现把临时数据的内容“偷过来”放到vector里:
int main() {std::string str1 = "aacasxs";std::vector<std::string> vec;vec.push_back(str1); // 传统方法,copyvec.push_back(std::move(str1)); // 调用移动语义的push_back方法,避免拷贝,str1会失去原有值,变成空字符串vec.emplace_back(std::move(str1)); // emplace_back效果相同,str1会失去原有值vec.emplace_back("axcsddcas"); // 当然可以直接接右值}// std::vector方法定义void push_back (const value_type& val);void push_back (value_type&& val);
