From C biancheng
http://c.biancheng.net/view/7868.html
C++11 标准中规定,通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为“万能引用”)。
template <typename T>
class A {
// 注意这里并非完美转发,因为T是类模板参数,而非函数模板参数
void foo(T&&); // 只能接收右值引用
template <typename U>
void bar(U&&); // 这里就是完美转发,可以接受左值/右值引用
};典型案例
#include <iostream>
using namespace std;
void foo(int& ) { cout << "lvalue\n"; }
void foo(int&&) { cout << "rvalue\n"; } // 这是普通的右值引用,非万能引用
template<class T>
void bar(T&& x) { // `T&&` 称为万能引用,此外 `auto&&` 也是万能引用的常见形式
foo(std::forward<T>(x)); // 使用 std::forward 配合万能引用实现完美转发
}
int main() {
int a = 1;
bar(a); // lvalue, 模板实例:bar<int&>(int&)
bar(1); // rvalue, 模板实例:bar<int>(int&&)
bar(std::move(a)); // rvalue, 模板实例:bar<int>(int&&)
return 0;
}通过将函数模板的形参类型设置为 T&&,我们可以很好地解决接收左、右值的问题。但除此之外,还需要解决一个问题,即无论传入的形参是左值还是右值,对于函数模板内部来说,形参既有名称又能寻址,因此它都是左值。那么如何才能将函数模板接收到的形参连同其左、右值属性,一起传递给被调用的函数呢?
std::forward解君愁
在bar函数内部,x已经具有名字,无论无何都是一个左值。只有通过std::forward才能将左/右值属性正确传递。下面是标准库中的实现,
/**
* @brief Forward an lvalue.
* @return The parameter cast to the specified type.
*
* This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
/**
* @brief Forward an rvalue.
* @return The parameter cast to the specified type.
*
* This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
" substituting _Tp is an lvalue reference type");
return static_cast<_Tp&&>(__t);
}可以看到有两个模板重载,对于左值和右值会分别匹配一个模板。返回值的类型都是万能引用。下面分类讨论,但须得注意,_Tp的类型是推导出来的,并非传参指定的(推导规则日后再叙)。
- 如果传了一个左值
int,_Tp的类型为int&,匹配第一个重载 - 如果传了一个右值
int,_Tp的类型为int&&,匹配第二个重载
接着,经过static_cast<_Tp&&>(__t),触发引用折叠。
- 第一个重载中,
__t的类型为int&,然后static_cast<int&&>(int&)折叠为int&,返回出去就得到一个左值引用,这保留了参数的左值属性。 - 第二个重载中,
__t的类型为int&&,然后static_cast<int&&>(int&&)折叠为int&&,返回出去就得到一个右值引用,这保留了参数的右值属性。
这个时候反过来看上面的典型案例,应该能够理解完美转发的原理了。
- 当传
a时,是一个左值,所以模板实例化bar<int&>(int&) - 传
1时,是一个右值,所以模板实例化bar<int>(int&&)或bar<int&&>(int&&) - 传
move(a)时,是一个右值,省略……
下面构造一段代码,触发第二个重载的断言,
#include <utility>
template<class T>
void g(T&& x) {
std::forward<T>(std::move(x)); // 故意写错
}
int main() {
int a = 1;
g(a); // #1
g<int&>(a); // #2
}上面代码中,1和2是等价的调用。因为a是左值,1推导出来就是2,2只是手工写出来而已。但是编译会报错,因为g的模板类型参数实例化为int&,而对forward的传参会调用接收右值版本的重载。所以会触发这个断言,
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
" substituting _Tp is an lvalue reference type");总结一下,std::forward的之所以能奏效归根结底是C++模板匹配/类型推导的能力,并非什么黑魔法。
引用折叠
C++不允许定义引用的引用。但在模板实例化,auto、decltype/using等类型推导时,经常会出现引用的引用。因此必须定义一套规则对此进行处理,这个规则就是引用折叠。具体规则为,
& + & = && + && = &&& + & = &&& + && = &&
一言蔽之,只有两个都是&&才会折叠为&&。
模板实例化触发引用折叠
void bar(int&) { cout << "L\n"; }
void bar(int&&) { cout << "R\n"; }
template <class T>
void foo(T&& x) {
std::is_lvalue_reference<T>::value ?
cout << "lvalue\n" :
cout << "rvalue\n";
bar(std::forward<T>(x));
bar(x); // note that the expression `x` is a lvalue
// whatever `x` itself is lvaue or rvalue.
}
int main() {
int a = 1;
foo(a);
foo(1);
return 0;
}
/* outputs:
lvalue
L
L
rvalue
R
L
*/- 当
T推导为int&,int& &&折叠为int& - 当
T推导为int&&,int&& &&折叠为int&&
auto 推导触发引用折叠
int a = 1;
auto r1 = a;
auto& r2 = a;
auto&& r3 = a;
auto&& r4 = 1;
cout << std::boolalpha;
cout << "r1 is lvalue reference? " << std::is_lvalue_reference<decltype(r1)>::value << "\n";
cout << "r2 is lvalue reference? " << std::is_lvalue_reference<decltype(r2)>::value << "\n";
cout << "r3 is lvalue reference? " << std::is_lvalue_reference<decltype(r3)>::value << "\n";
cout << "r4 is lvalue reference? " << std::is_lvalue_reference<decltype(r4)>::value << "\n";
/* outputs:
r1 is lvalue reference? false
r2 is lvalue reference? true
r3 is lvalue reference? true
r4 is lvalue reference? false
*/- 普通auto会去处cv修饰和引用,auto=int,r1推导为int,没问题
- 普通auto依然推导为int,auto=int,(a是左值,也可推导为int&,加上引用得到int& &,引用折叠后)得到r2的类型是int&
- 万能引用auto,此时auto=int&(a是左值),加上引用得到int& &&,引用折叠后r3的类型为int&
- 万能引用auto,auto=int,(也可推到为int&&,加上引用得到int&& &&,引用折叠后)r4的类型为int&&
注意,这里有一个原则。对于r2,其实auto推导为int&,引用折叠后r2的类型还是int&,秉着越简单越好的原则,直接将auto推导为int。对于r4,由于1是纯右值,所以auto可以推导为int&&,但还有更简单的int,也不影响最终r4的类型,所以编译器倾向于将auto推导为int。
decltype/using 触发引用折叠
int a = 1;
decltype(a) r1 = a;
decltype((a)) r2 = a;
decltype((a))&& r3 = a;
using MyType = decltype((a));
MyType& r4 = a;
// MyType&& r5 = 1; // int& && -> int& -> err:
// Non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'
decltype(move(a)) r5 = 1; // ok, r5 is int&&
decltype(move(a))& r6 = 1; // same error above
decltype(move(a))&& r7 = 1; // ok, r7 is int&& && -> int&&
cout << std::boolalpha;
cout << "r1 is lvalue reference? " << std::is_lvalue_reference<decltype(r1)>::value << "\n";
cout << "r2 is lvalue reference? " << std::is_lvalue_reference<decltype(r2)>::value << "\n";
cout << "r3 is lvalue reference? " << std::is_lvalue_reference<decltype(r3)>::value << "\n";
cout << "r4 is lvalue reference? " << std::is_lvalue_reference<decltype(r4)>::value << "\n";- r1推导为int
- 表达式
(a)是左值,r2推导为int& - r3推导为int& &&,引用折叠为int&
- r4推导为int& &,引用折叠为int&
小结
引用折叠只发生在类型推导的过程中。常出现在模板实例化、auto、decltype、using等场景。
One more pitfall
在典例代码中,如果,
int a = 1;
int&& b = move(a);
bar(b);
会输出什么呢?
Answer
会输出“lvaue”。因为虽然
b的类型是右值,但表达式b是一个左值,因为他有名字。另外,从移动语义上看,他已经窃取了a的值。它是将亡值a的延续。所以b是一个左值。