我们对引用都不陌生,引用准许我们为已存在的对象创建别名。若访问或修改创建的引用,全部会直接作用到指涉它的对象本体。
C++11之前,只存在一种引用——左值引用(lvalue reference)。
左值指可以在赋值表达式等号左边出现的元素,包括具名对象、在栈数据段和堆数据段上分配的对象、其他对象的数据成员,或一切具有存储范围的数据项(l指的是location)。
右值指只能在赋值表达式等号右边出现的元素,如字面值或临时变量。
左值引用只可以绑定左值,所以以下代码编译失败:
int& i=42;
但一般可以将右值绑定到const左值引用上:
int const& i = 42;
在现实中,代码却要向接受引用的函数传入临时变量,因而早期C++破例特许绑定方式,让参数发生隐式转换:
void print(std::string const& s);
print("Hello!");
C++11接纳了右值引用特性,它只与右值绑定,且声明改为“&&”:
int&& i=42;
int j=42;
int&& k=j;//编译失败
右值引用是实现移动语义的基础。
移动语义
假设我们预先知道函数接收右值参数,可以自由改变,我们只需移动右值参数而不必复制本体,能省去更多内存操作。考虑一个自定义类,其默认构造函数申请一大块内存:
#include<iostream>
class X
{
private:
int* data;
public:
X():data(new int[10000])
{}
~X()
{
delete[]data;
}
X(const X& other) :data(new int[10000])//复制构造函数
{
std::copy(other.data, other.data + 10000, data);
}
X(X&& other) :data(other.data)//移动构造函数
{
other.data = nullptr;
}
};
本例展示的移动构造函数,按右值引用的方式接收源实例,复制data指针,将源实例的data改为空指针,节约了一大块内存,也省去了复制时间。
若某个对象不再有用处,想通过移动语义移出,则可通过std::move()或static_cast<X&&>转换为右值:
X x1;
X x2 = std::move(x1);
X x3 = static_cast<X&&>(x2);
void do_stuff(X&& x_)
{
X a(x_);//复制构造
X b(std::move(x_));//移动构造
}
int main()
{
do_stuff(X());//正确,X()生成匿名对象
X x;
do_stuff(x);//错误,具名对象x是左值,无法与右值引用绑定
}
移动语义在线程库中大量使用,可以取代不合理的复制语义,也可以实现资源转化。std::thread、std::unique_lock<>、std::future<>、std::promise<>和std::packaged_task<>等类无法复制,但含有移动构造函数,可以按转移方式充当函数返回值,在实例间转移资源,std::thread实例的归属权转移就需要移动语义。
若源对象显式移动到另一对象,源对象只会被销毁或重新赋值。按照良好的编程经验,类需确保不变量的成立范围覆盖“移出状态”,以std::string为例,假设它的实例作为数据源参与移动操作,操作完成后,这一实例需保持某种“合法、有效”的状态。(Effective ModernC++29)
许多std::string(少于15个字符)的实现都采用了小型字符串优化(small string optimization), SSO。采用了SSO以后,“小型”字符串会存储在的std::string对象内的某个缓冲区内,而不去使用堆上分配的存储。在使用了基于SSO的实现的前提下,对小型字符串实施移动并不比复制更快。(参考《Effective ModernC++》)
右值引用和函数模版
假设某函数模版,模版参数是接收参数的类型,其自动类型推导机制:若给出左值作为函数参数,模版参数会被推导为左值引用;若给出右值,则推到为无修饰的普通引用。
template<typename T>
void foo(T&& t)
{}
//传入右值
foo(42); //调用foo<int>(42);
foo(3.14159); //调用foo<double>(3.14159)
foo(std::string()); //调用foo<std::string>(std::string())
//传入左值
int i = 42;
foo(i); //调用foo<int&>(i)
根据函数声明,其参数型别是T&&,传入左值会被解释为“引用的引用”,发生引用折叠(reference collapsing), 所以其函数签名是:void foo<int&>(int& t);
同一个函数模版既能接收左值,也能接收右值,传入左值则对象会被复制到相应线程内部空间,传入右值则会按移动方式传递。