转载自:《C++primer》
右值引用
为了支持移动操作,新标准引入了一种新的引用类型----右值引用(rvalue reference),所谓右值引用就是必须绑定到右值的引用。我们通过&&而不是&来获得右值引用,右值引用有一个重要的性质----只能绑定到一个将要销毁的对象。因此,我们可以自由地将一个右值引用的资源“移动”到另一个对象中。
一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值,类似任何引用,右值引用也是某个对象的另一个名字,对于常规引用(左值引用),不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式,右值引用有着完全相反的特性,我们可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上。
int i = 42;
int& r = i; //正确:r引用i
int&& rr = i; //错误:不能将一个右值引用绑定到一个左值上
int& r2 = i * 42; //错误:i*42是一个右值
const int& r3 = i * 42; //正确:我们可以将一个const的引用绑定到一个右值上
int&& rr2 = i * 42; //正确:将rr2绑定到乘法结果上
返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。我们不能将一个左值引用绑定到这类表达式上,但我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。
左值与右值
左值持久,右值短暂。左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中闯将的临时对象。
由于右值引用只能绑定到临时对象,所有它有以下特性:
1.所引用的对象将要被销毁
2.该对象没有其他用户
这俩个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。
变量是左值,变量可以看做只有一个运算对象而没有运算符的表达式,变量表达式也有左值/右值属性,变量表达式都是左值,带来的结果就是,我们不能将一个右值引用绑定到一个右值引用类型变量上。
int&& rr1 = 42; //正确:字面常量是右值
int&& rr2 = rr1; //错误:表达式rr1是左值
标准库move函数
虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显式地将一个左值转换为对应的右值引用类型(不推荐),我们还可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用。此函数定义在头文件utility中。
int&& rr1 = 42;
int&& rr2 = (int&&)rr1; //不推荐
int&& rr3 = std::move(rr1);
move调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它,我们必须认识到,调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它。在调用move之后,我们不能对移后源对象的值做任何假设(不能使用一个移后源对象的值)。
调用限定语std::move,不提供using声明(避免冲突)
标准库move函数和forward函数都是模版函数,在标准库的定义中它们都接受一个右值引用的函数形参,在函数模板中,右值引用形参可以匹配任何类型,如果我们的应用程序也定义了一个接受单一形参的move函数,则不管该形参是什么类型,应用程序的move函数都将与标准库的版本冲突,forward函数也是如此。因此move(以及forward)的名字冲突要比其他标准库函数的冲突频繁的多,所以建议使用带限定语的完整版本。
std::move的底层实现
标准库move函数是使用右值引用的模板的一个很好的例子,虽然不能直接将一个右值引用绑定到一个左值上,但可以用move获得一个绑定到左值上的右值引用,由于move本质上可以接受任何类型的实参,因此我们不会惊讶于它是一个函数模板。
标准库是这样定义move的:
template<typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
remove_reference(标准类型转换模板):remove_reference模板有一个模板类型参数和一个名为type的(public)类型成员,如果用一个引用类型实例化该模板,则type将表示被引用的类型,例如实例化remove_reference<int&>,则type成员将是int,remove_reference::type脱去引用,剩下元素类型本身,注意type是一个类的成员,而该类依赖于一个模板参数,因此,我们必须在返回类型的声明中使用typename来告知编译器,type表示一个类型。
这段代码中move的函数参数T&&是一个指向模板类型参数的右值引用,通过引用折叠,此参数可以与任何类型的实参匹配,特别是,我们既可以传递给move一个左值,也可以传递给它一个右值。
string s1("hi!"), s2;
s2 = std::move(string("bye!")); //正确:从一个右值移动数据
s2 = std::move(s1); //正确:但在赋值之后,s1的值是不确定的
std::move(string("bye!")):
- 推断出的T的类型为string(当向一个右值引用参数传递一个右值时,由实参推断出的类型为被引用的类型)
- remove_reference用string进行实例化
- remove_reference<string>的type成员是string
- move返回的类型是string&&
- move的函数参数t的类型为string&&
因此这个调用实例化move<string>,即函数 string&& move(string&& t),函数体返回static_cast<string&&>(t),t的类型已经是string&&,因此调用结果就是它所接受的右值引用。
std::move(s1):
- 推断出T的类型为string&(string的引用,而非普通string)
- 因此,remove_reference用string&进行实例化
- remove_reference<string&>的type成员是string
- move的返回类型仍是string&&
- move的函数参数t实例化为string& &&,会折叠为string&
因此这个调用实例化move<string&>,即 string&& move(string& t),这正是我们所寻求的----我们希望将一个右值引用绑定到一个左值,此情况下t的类型为string&,cast将其转换为string&&。
左值static_cast到一个右值引用
通常情况下,static_cast只能用于其他合法的类型转换,但是,这里又有一条针对右值引用的特许规则:虽然不能隐式地将一个左值转换为右值引用,但我们可以用static_cast显式地将一个左值转换为一个右值引用。对于操作右值引用的代码来说,将一个右值引用绑定到一个左值的特性允许它们阶段左值,截断一个左值是安全的(为什么安全?),而且通过允许进行这样的转换,C++认可这种做法。统一使用std::move使得我们在程序中查找潜在的截断左值的代码变得很容易。