移动语义与右值引用
1.值类别
1)左值
- 具名,可取地址
- 非常
non-const
左值可以放在赋值运算符的左侧 - 常见情况
- 变量
- 左值对象的成员
- 返回左值引用的表达式,如
++x
、x = 1
- 字符串字面量,如
"abc"
- 字符串字面值为左值一个最重要的原因是可以获取其地址,
cout << &"abc" <<endl;
可以正常编译并运行。这是因为C++将字符串左值实现为char
型数组,为其分配了空间并且允许程序员对其进行操作
- 字符串字面值为左值一个最重要的原因是可以获取其地址,
2)纯右值
- 不具名、不能取地址的“临时对象”
- 不可以放在赋值运算符的左侧
- 常见情况
- 返回类型非引用的函数调用或运算符表达式,如
x++
、1 + 2
- 除字符串字面量外的字面量,如
true、 42
lambda
表达式
- 返回类型非引用的函数调用或运算符表达式,如
3)将亡值
- C++11引入,和纯右值合称为“右值”
- 不可以放在赋值运算符的左侧
- 常见情况
- 右值对象或者数组的成员
- 返回右值引用的表达式,如
move(x)——转换为右值引用 若x是int,则move(x)是int &&
2.重要的成员函数重载
1 | // 拷贝,当传入参数为左值时调用拷贝构造 |
3.移动的意义
- 允许资源的传递
- 允许返回大对象和容器
- 一般同时使用异常来表示错误
4.移动和 noexcept
noexcept
表示函数不会抛出异常
下列成员函数一般不允许抛出异常
- 析构函数
- 移动构造函数,如果移动构造没有标成
noexcept
,比如说vector
在动态调整大小的时候都不会调用移动构造在移动元素 - 移动赋值运算符
- 交换函数(
swap
)
5.五法则
因为用户定义析构函数、拷贝构造函数、拷贝赋值运算符的存在阻止移动构造函数和移动赋值运算符的隐式定义,所以任何想要移动语义的类应当声明全部五个特殊成员函数。
6.右值引用的误用
1 | Obj&& wrong_move(){ |
7.坍缩规则和转发引用
Q:Vector(Vector&& rhs)
这里的 rhs
是左值还是右值?
A:右值引用变量有标识符,所以是左值
——所以使用右值引用调用其他函数需要加上 move()
保持右值属性
Q:在通用的函数模板里怎么办?
A:分别写两个不同的重载
1 | template<typename T> |
问题:重复、啰嗦
引入转发引用
坍缩规则
T& & → T&
、T& && → T&
、T&& & → T&
、T&& && → T&&
所以只有
T&& &&
会转化为右值引用所以
T&&
不是右值引用,当其出现在函数模板的参数或者变量声明中时T&&
是转发引用std::move(x)
:把x转换成右值引用std::forward<T>(x)
:保持x的引用类型——传进来的T是左值,进函数的也是左值,右值也一样
1 | // forward使用示例 |
8.临时对象的生命周期
1 | class result; |
**生命期延长规则 **
如果一个
prvalue
(纯右值) 被绑定到一个引用上,它的生命周期会延长到跟这个引用变量一样长result&& r=process_shape(circle(),triangle());
则右边这个临时对象的生命周期会延长到
r
离开作用域如果是
xvalue
(将亡值) ,则不能延长result&& r=move(process_shape(circle(),triangle()));
这种写法是错误的
C++对象的自动生命周期
- 后创建的先析构
- 全局对象和静态对象在进入
main
之前创建 - 函数静态对象在第一次执行到声明语句时创建
- 函数自动对象在定义时创建,到定义的所在的
}
即析构 - 临时对象在当前语句执行完成后即析构(除非赋值给引用变量而延长生命期)
经典习题:析构顺序
1 | C c; |