C++11的值类别
在C++11中,除了原本的左值(lvalue),纯右值(rvalue),还加入了一个新的将亡值(xvalue)
本文试图分析以下问题:
- 为什么要引入移动构造函数
- 怎么理解左值和纯右值
- 怎么理解将亡值
为什么要引入移动构造函数
C++在C++98/03是不推荐用stl的,因为很多类的效率都很低下
这种效率低下主要体现在对临时值额外的拷贝操作(模板的低效转发也是一方面,本文不讨论这个),因为const T&虽然可以捕获临时值,但是仅仅只是延长了他的生命周期,你只能拷贝他,而不能直接使用它。
看一下使用C++98/03是如何实现一个自己的string的
1 | #include <iostream> |
输出
1 | default construct |
1 调用了一次默认构造函数用于构造a
2 调用了char为参数的构造函数(调用了一次new,并且复制了一次内存)
3 调用了一次copy(调用了一次new,并且复制了一次内存)
可以看到,只是一次赋值操作,会产生两次new和内存复制。这是因为const MyString&无法判断传入的是一个以后会被使用的左值,还是一个临时的右值
C++11为了填这个坑,新加了移动构造函数,当遇到临时值时,会优先调用移动构造函数。
如下
1 | public: |
输出
1 | default construct |
1 调用了一次默认构造函数用于构造a
2 调用了char为参数的构造函数(调用了一次new,并且复制了一次内存)
3 调用了一次move(没有new,没有内存拷贝)
可以看到,通过对临时值的判断,就能提高了一倍的性能。
而这种临时值,在C++11中有了新的定义,叫做将亡值
小结
加入移动构造函数是为了优化对临时值(将亡值也能看作是临时值)的识别,从而优化性能
怎么理解左值和纯右值
这里直接引用cppreference的定义
- has identity: it's possible to determine whether the expression refers to the same entity as another expression, such as by comparing addresses of the objects or the functions they identify (obtained directly or indirectly);
- can be moved from: move constructor, move assignment operator, or another function overload that implements move semantics can bind to the expression.
In C++11, expressions that:
- have identity and cannot be moved from are called lvalue expressions;
- do not have identity and can be moved from are called prvalue ("pure rvalue") expressions;
两个属性,拥有身份和能被移动
左值是拥有身份和不能被移动的
纯右值是不拥有身份和能被移动的
拥有身份好理解,就是是否可以用&来被取地址(来判断和别的表达式是否同一个实体)。
但什么是能被移动的?什么是能被移动构造函数绑定的?这就变成先有鸡还是先有蛋的问题了,按我的理解,就是是否能把数据所有权交接给别人的表达式
简单来说:
- 凡是能被&取地址的且不能把数据所有权交给别人的,就是左值
- 凡是临时值,就是右值。
- 凡是字面值(字符串除外),就是右值。(字面值本身是不可被修改的,因此可以move给别人)
cppreference有很多举例,不过最有意思的还是下面例子:
- i++是右值,而++i是左值
乍一看很奇怪,从操作符重载的角度就很容易理解了
其他的各种操作符的左右值分辨通过这种方式也很容易能够理解
1 | class Time{ |
怎么理解将亡值
分清楚左值和纯右值以后,将亡值就容易理解多了。
左值牢牢把控着对数据的所有权,保证不会被移动构造函数move走
当我们不需要再使用某个左值的时候(常见于各种中间tmp变量),可以用std::move()函数将它强制变成将亡值。
这其实也可以理解成一种临时值,此时会优先匹配移动构造函数。