引用折叠
我们知道,将一个左值传递给右值引用参数是不合法的,因为通常右值引用不能绑定到左值上,但是有两个例外。
- 右值引用指向模板类型参数,如
T&&
,这种引用也叫做万能引用,因为它既可以被传入左值,也可以被传入右值。
template <typename T>
void f(T&& t) {}
当一个左值被传递给 T&& 参数时,如 int i
,f(i)
进行调用,T
会被推断为 int&
,而不是 int
,这样 f
的参数就变成了 int& &&
好像是一个引用的引用,这应该是不允许的啊。
事实上,我们确实不能直接定义一个引用的引用,但是通过模板参数间接定义是可以的。
- 如果定义出一个引用的引用,则这些引用会形成 折叠
折叠规则:
对于一个给定的类型 X
X& &
,X& &&
,X&& &
会被折叠为X&
X&& &&
会被折叠为X&&
只有右值引用的右值引用折叠后还是右值引用,其他只要沾上左值引用,最后都会被折叠为左值引用。
注意:引用折叠只能用于间接创建的引用的引用,如类型别名,或模板参数,auto。
所以,T&&参数,如果传入左值,会被折叠成 int&
,传入右值,则会被折叠成 int&&
,如果在 f
函数体内部需要将参数 t
传给其他具有万能引用的函数,那么由于右值引用和左值引用本身都是左值,传入到其他函数后会都被折叠成左值引用类型,如果要保持其引用类型不变,则需要进行完美转发。
move如何工作
我们常用 std::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 - C++ Reference (cplusplus.com))
使用它可以将类型的引用去除,remove_reference
有一个模板类型参数和一个名为 type
的类型成员,如果我们使用一个引用类型实例化 remove_reference
,则 type
将表示去除引用后的类型。如 remove_reference<int&>::type
就是 int
类型。
注意,使用时必须在前面加上 typename
以告知编译器 type
是一个类型成员。因为此时编译器不知道 remove_reference<T>
是一个什么样的类型,因为它还没有被实例化,其内部的 type
可能是 static
数据成员(编译器默认认为),也可能是类型成员,无法确定。需要程序员显式地使用 typename
告知才能通过编译。
static_cast
用于强制类型转换,具体介绍在我的上一篇博客:[C++](27)强制类型转换 static_cast reinterpret_cast const_cast dynamic_cast_世真的博客-CSDN博客
如下两个例子,分别将右值和左值传入 move
string s1("hi!"), s2;
s2 = std::move(string("bye!")); //传入右值
s2 = std::move(s1); //传入左值
第一个赋值(传入右值):
-
函数参数为
T&&
,T
可以非常合理地被推断为string
-
remove_reference<string>::type
非引用类型转换后不变,还是string
-
所以最后
move
被实例化为:string&& move(string&& t) { return static_cast<string&&>(t); }
-
t
本身就是右值引用,所以强制类型转换什么也不做,最后返回的还是右值引用。
第二个赋值(传入左值):
-
万能引用,编译器会将
T
推断为string&
-
函数参数
T&&
变成string& &&
,会折叠为string&
-
remove_reference<string&>::type
就是string
-
所以最后 move 被实例化为:
string&& move(string& t) { return static_cast<string&&>(t); }
-
这样,左值引用正好被转换成了右值引用。
这里的核心在于 static_cast
,它可以将左值转换到右值引用。我们知道右值引用不能绑定到左值,这其实只是不允许进行隐式类型转换,而使用 static_cast
进行显式地转换是允许的。
此时,如果用一个对象 s2 来接收 move 的返回值,由于返回的是右值引用,那么就会调用 s2 的移动赋值运算符,将 s1 的资源转移给s2。
虽然允许我们自己使用static_cast
将左值转换到右值类型,但还是建议使用标准库的 std::move
,更加方便易读。