右值引用和移动语义

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。

右值引入

在以前我们知道有引用的语法,我们通常叫做左值引用,那么什么是左值呢?可以看下边的几个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main()
{
// 左值:可以取它的地址
/*int a = 10;
const int b = 20;
int* p = &a;
*p = 100;*/

// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;

// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;

double x = 1.1, y = 2.2;
}

可以看到,左值,可以获取它的地址+可以对它赋值。当然定义时用const修饰的左值,也不能对他赋值,但是可以取地址。左值引用呢,就是给左值的引用。

知道左值后,那么什么是右值呢?右值的形式是什么?右值有什么实际价值?看几个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main()
{
// 右值:不能取地址
10;
x + y;
fmin(x, y);
//cout << &10 << endl;
//cout << &(x + y) << endl;

// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);

// 这里编译会报错
10 = 1;
x + y = 1;
fmin(x, y) = 1;
}

以上几个例子都是右值,右值也是一个表达数据的表达式,如字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号右边,但是不能出现在赋值符号的左边,右值不能取地址

右值引用就是对右值的引用,给右值取别名

左值右值

左值引用可以引用右值吗?右值引用可以引用右值吗?

1
2
3
4
5
6
7
8
// 有条件的支持
// 左值引用可以引用右值吗? const的左值引用可以
//double& r1 = x + y;
const double& r1 = x + y;

// 右值引用可以引用左值吗?可以引用move以后的左值
//int&& rr5 = b;
int&& rr5 = move(b);

这里注意:

11是字面常量,Func函数参数列表是个左值引用,所以会报错,两种修改方法:

  • Func(const T& x)
  • Func(T&& x)

第一种方法:const的左值引用可以接收右值

1
2
3
4
// x既能接收左值,也能接收右值
template<class T>
void Func(const T& x)
{}

左右值总结

左值引用总结

  1. 左值引用只能引用左值。
  2. 但是const左值引用既可引用左值,也可引用右值。

右值引用总结

  1. 右值引用只能引用右值,不能引用左值
  2. 右值引用可以引用move之后的左值

注意

rr1和rr2可以取地址了,它们是左值了。

左值的不足

引用的价值:减少拷贝

左值引用解决哪些问题?

  1. 做参数。a、减少拷贝,提高效率 b、做输出型参数
  2. 做返回值。 a、减少拷贝,提高效率 b、引用返回,可以修改返回对象(比如:operator[])

但是,C++98的左值引用面向下边的场景很难进行处理:

右边的写法虽然解决了问题,但是并不是太符合使用习惯

具体例子

不加移动构造的string

加移动构造的string类

不加移动构造移动赋值的string

加移动构造移动赋值的string类

总结

移动构造和移动赋值解决了传值返回这些类型对象的问题,STL的各个容器在C++11增加移动构造和移动赋值。移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

完美转发

模板中&&万能引用

在模板中,&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值。

先看个例子:

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
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }

void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}

int main()
{
PerfectForward(10); // 右值

int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值

const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值

return 0;
}

在我们的预期中,Func函数应该是,左值打印左值,右值打印右值,但是运行结果确如下图所示:

这里可以看到,所有的都成了左值引用,根本没有调用右值引用的版本,这个就是引用折叠的问题。

std::forward

我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用完美转发。

针对上边的代码只需要将,T类型的参数t完美转发一下就可以了,std::forward 完美转发在传参的过程中保留对象原生类型属性

1
2
3
4
5
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}

运行结果如下:

可以看到完美转发的效果,左值调用左值,右值调用右值。

实际应用例子

在我们模拟实现的list中测试,移动构造

list插入"world"(右值),代码调用层级如下

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
void push_back(T&& x) 
{
insert(end(), std::forward<T>(x)); //完美转发保留右值属性
}

// 调用insert,这里也是万能引用接收

iterator insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;

//这里调用节点的构造函数,也同样要完美转发,保留右值属性
Node* newnode = new Node(std::forward<T>(x));

// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;

return iterator(newnode);
}
// insert调用了链表节点的构造,同样用万能引用接收

list_node(T&& x)
:_data(std::forward<T>(x)) //完美转发
, _next(nullptr)
, _prev(nullptr)
{}

最后的节点的构造,会调用string类的构造,所以那里同样需要完美转发,调用了string类的右值构造

1
2
3
4
5
6
7
8
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 资源转移" << endl;
swap(s);
}