现代C++:右值引用、移动语义与完美转发
# 现代C++:右值引用、移动语义与完美转发
# 右值引用
# 右值与左值
左值,常常位于赋值符号左边,通常对应着一个长时间存在于内存中的变量;右值,常常位于赋值符号右边,通常是一个表达式,代表计算过程中临时生成的中间变量,即将消失的,不长时间存在于内存中的值。C++11 中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值、将亡值
纯右值,纯粹的右值,要么是纯粹的字面量,要么是求值结果相当于字面量或匿名临时对象。非引用返回的临时变量、运算表达式产生的临时变量、原始字面量、Lambda表达式都属于纯右值
将亡值,是 C++11 为了引入右值引用而提出的概念,也就是即将被销毁、却能够被移动的值
要拿到一个将亡值,就需要用到右值引用:T &&,右值引用的声明让这个临时值的生命周期得以延长、只要变量还活着,那么将亡值将继续存活
C++11 提供了
std::move
这个方法将左值无条件转换为右值
# 常值修饰符,使用int const &
int const
和 int
可以看做是两个不同的类型,C++ 规定 int &&
能自动转换成 int const &
,但不能转换成 int &
,因而,使用 int const &
也可以延长将亡值的生命周期。
小技巧:如果有移动赋值函数,可以删除拷贝赋值函数 当拷贝赋值函数被删除后,对于
v2 = v1;
编译器会尝试v2 = List(v1);
,从而变成了移动语义,调用移动赋值函数。
# 移动语义
传统 C++ 通过拷贝构造函数和赋值操作符为类对象设计了拷贝/复制的概念,但为了实现对资源的移动操作,调用者必须使用先复制、再析构的方式,这是非常反人类的一件事情,造成了大量的时间和空间上的浪费。
class A {
public:
int *pointer;
A():pointer(new int(1)) {
std::cout << "构造" << pointer << std::endl;
}
A(A& a):pointer(new int(*a.pointer)) {
std::cout << "拷贝" << pointer << std::endl;
} // 无意义的对象拷贝
A(A&& a):pointer(a.pointer) {
a.pointer = nullptr;
std::cout << "移动" << pointer << std::endl;
}
~A(){
std::cout << "析构" << pointer << std::endl;
delete pointer;
}
};
# 完美转发
#include <iostream>
#include <utility>
void reference(int& v) {
std::cout << "左值引用" << std::endl;
}
void reference(int&& v) {
std::cout << "右值引用" << std::endl;
}
template <typename T>
void pass(T&& v) {
std::cout << " 普通传参: ";
reference(v);
std::cout << " std::move 传参: ";
reference(std::move(v));
std::cout << " std::forward 传参: ";
reference(std::forward<T>(v));
std::cout << "static_cast<T&&> 传参: ";
reference(static_cast<T&&>(v));
}
int main() {
std::cout << "传递右值:" << std::endl;
pass(1);
std::cout << "传递左值:" << std::endl;
int v = 1;
pass(v);
return 0;
}
结果为:
传递右值:
普通传参: 左值引用
std::move 传参: 右值引用
std::forward 传参: 右值引用
static_cast<T&&> 传参: 右值引用
传递左值:
普通传参: 左值引用
std::move 传参: 右值引用
std::forward 传参: 左值引用
static_cast<T&&> 传参: 左值引用
由于 v 是一个引用,所以同时也是左值。 因此 reference(v)
会调用 reference(int&)
,输出『左值』。
对于 pass(l)
,左值为什么可以成功传递给 pass(T &&)
?
这是基于引用坍缩规则:
函数形参类型 | 实参参数类型 | 推导后形参类型 |
---|---|---|
T& | 左引用 | T& |
T& | 右引用 | T& |
T&& | 左引用 | T& |
T&& | 右引用 | T&& |
总之,无论模板参数是什么引用,当且仅当实参类型为右值引用,模板参数才能被推导为右值引用类型。
完美转发就是基于上述规则,会保持原来的参数类型。我们应该使用 std::forward
对参数进行传递。