右值引用
Published on
左值与右值
C++03标准
C++03标准中将表达式分为左值与右值,并且非左即右:
- 左值:表达式结束后依然存在的持久对象,可以出现在赋值号的左侧与右侧,可以寻址
- 右值:表达式结束后就不再存在的临时对象,只可以出现在赋值号的右侧,不可寻址
C++11标准
C++11对左值、右值的分类重新进行了定义,在C++11标准中,表达式有以下两个属性:
- 有身份:指代某个非临时对象
- 可被移动:可被右值引用类型匹配
根据以上两个属性可以得到4种组合:
- 有身份,不可移动:左值,与C++03中的左值相同
- 无身份,可移动:纯右值,与C++03中的右值相同
- 有身份,可移动:将亡值,即生命周期即将结束的值,为C++11中新增加的类型
- 无身份,不可移动:无意义
将亡值与纯右值合称为右值。
右值引用
C++11之前的引用类型全部为左值引用,用&符号声明。一般来说,左值引用必须绑定左值:
// num为左值
int num = 10;
// 正确,a为左值num的引用
int &a = num;
// 错误,10为右值,不能使用左值引用绑定
int &b = 10;
但是有个例外,可以将const左值引用绑定至右值:
const int &c = 10;
C++11中添加了右值引用类型,用&&符号声明,右值引用只能绑定右值:
// num为左值
int num = 10;
// 错误,右值引用不能绑定左值
int &&a = num;
// 正确,右值引用绑定纯右值
int &&b = 10;
右值引用的主要作用是用于实现移动语义与完美转发。
移动语义
在没有右值引用之前,C++中只存在“拷贝语义”,通过使用拷贝构造函数或拷贝赋值运算符(它们的参数为左值引用)可以将一个对象进行拷贝:
class A
{
public:
A() {}
~A() {}
// 拷贝构造函数
A(const A &rhs) {}
// 拷贝赋值运算符
A &operator=(const A &rhs) { return *this; }
};
int main()
{
A a1;
// 使用拷贝构造函数进行对象拷贝
A a2(a1);
A a3 = a1;
// 使用拷贝赋值运算符进行对象拷贝
A a4;
a4 = a1;
}
此时,如果要将一个即将析构的对象赋值给一个新对象,仍然需要先进行拷贝再进行析构,这样运行效率较低。由于原对象即将析构,因此可以将其中的资源移动至新对象,从而减少一次资源复制。
在增加了右值引用后,可以为一个类添加参数为右值引用的构造函数与赋值运算符,即移动构造函数与移动赋值运算符,通过它们可以将一个右值对象的资源移动至新对象,从而提高运行效率,这就是“移动语义”:
class A
{
public:
A() {}
~A() {}
// 移动构造函数
A(A &&rhs) {}
// 移动赋值运算符
A &operator=(A &&rhs) { return *this; }
};
int main()
{
A a1;
// 错误,未声明拷贝构造函数
A a2 = a1;
// 正确,使用std::move()将a1转换为右值引用,调用移动构造函数
A a2 = std::move(a1);
// 正确,A()为右值,调用移动构造函数或移动赋值运算符
A a4{A()};
A a5;
a5 = A();
}
在上面的例子中,a1为左值,所以如果直接执行A a2 = a1
会调用拷贝构造函数,如果拷贝构造函数未定义则会报错。如果要强制调用移动构造函数,则需要使用std::move函数将a1转换为右值引用。std::move函数的实现大致如下:
template <typename T>
typename remove_reference<T>::type &&move(T &&t)
{
return static_cast<typename remove_reference<T>::type &&>(t);
}
可以看出,它只是将输入参数强制转换为了底层类型的右值引用。
万能引用
发生类型推导(例如模板、auto)的时候,使用T&&类型表示万能引用,万能引用类型的形参能匹配任意引用类型的左值、右值。例如:
template <class T>
void func(T &&t)
{
return;
}
int main()
{
A a1, a2;
// 匹配左值引用
func(a1);
// 匹配右值引用
func(std::move(a2));
}
引用折叠
在模板编程中,如果模板展开后出现双重引用,则需要进行引用折叠。折叠规则为,如果任一引用为左值引用,则结果为左值引用,否则为右值引用,即:
- A& & -> A&
- A&& & -> A&
- A& && -> A&
- A&& && -> A&&
根据此规则,在万能引用的例子中,T的类型推导为:
// T被推导为A&,T&&展开后为A&,与a1相同
func(a1);
// T被推导为A&&,T&&展开后为A&&,与std::move(a2)相同
func(std::move(a2));
完美转发
无论是左值引用还是右值引用,它们本身都是左值。因此在上述使用了万能引用的函数中,如果将该参数转发给另一个函数,则无论参数是什么类型,都会当作左值传入。此时需要使用std::forward函数进行完美转发:
template <class T>
void func(T &&t)
{
do_something(std::forward<T>(t));
}
std::forward函数的实现大致如下:
template <typename T>
T &&forward(T ¶m)
{
return static_cast<T &&>(param);
}
可以看出,通过使用引用折叠,达到了当输入是左值引用时,返回左值引用,当输入是右值引用时,返回右值引用的目的,从而实现了完美转发。