引子
声明一个模板函数func()
,在main()
中调用它:template<typename T>
int func(T&& v)
{
return v;
}
在main()
中调用它:int main()
{
int a = 1;
func<int>(a); // compile error
func<int>(std::move(a));
return 0;
}
可以看到func<int>(a)
这一行编译报错:error: cannot bind rvalue reference of type ‘int&&’ to lvalue of type ‘int’
,原因显而易见:int func(T&& v)
被特化成了int func(int&& v)
,只接受右值。
取消显式地声明模板参数,尝试让编译器推导参数类型:int main()
{
int a = 1;
func(a);
func(std::move(a));
return 0;
}
居然编译可以成功,说明func(T&& v)
可以接受左值也可以接受右值,那么此时func(T&& v)
中的T&&
是什么呢?
Universal Reference
在上述例子中,T&&
不再单纯表示T
类型的右值引用,而是万能引用(universal reference)。T&&
可能被推导为左值引用也可能被推导成右值引用。
出现universal reference的场景必须是不能确定引用类型的,变量的实际类型需要进行类型推导得出:
- 模板函数中参数为
T&&
使用
auto&& v
std::vector<T>&&
作为参数只能表示右值引用,不是一个universal reference。因为无论T
被推导成什么,参数永远都是个vector
类型的右值引用const T&&
只能表示常右值引用,不是一个universal reference。关于为什么const T&&
只能表示右值详见:Why adding const makes the universal reference as rvalue
不是所有形如T&&
的模板参数类型都是universal reference,关键还是要看是否涉及类型推导。看一个STL vector的例子:template<typename value_type, typename Allocator = allocator<value_type>>
class vector
{
void push_back(value_type &&__x);
void emplace_back(_Args&&... __args)
}
对于push_back
,由于value_type
在声明vector
对象时是必填项,所以此处value_type
类型已经确定,value_type &&__x
不涉及类型推导,是一个右值引用,不是universal reference。
对于emplace_back
,类型_Args
独立于value_type
,无法在vector
被声明时判断类型,需要推导类型,此处是_Args&&... __args
是一个universal reference。
对于模板引用,折叠的规则如下:&& && -> &&
& && -> &
& & -> &
&& & -> &
可以观察发现只有T&&
随着T
类型的变化而变化,所以T&&
被用作universal reference
std::forward
在下述例子中,定义两个同名函数,分别接受一个左值和右值:template<typename T>
void print(T&& v)
{
std::cout << "rvalue v = " << v << std::endl;
}
template<typename T>
void print(T& v)
{
std::cout << "lvalue v = " << v << std::endl;
}
template<typename T>
int func(T&& v)
{
print(v)
return v;
}
int main()
{
int a = 1;
func(a);
func(std::move(a));
return 0;
}
对于func
传入的左值和右值,预期应该是输出:lvalue v = 1
rvalue v = 1
但是实际上输出是:lvalue v = 1
lvalue v = 1
原因是因为func(T&& v)
中的参数v
在print
看起来也是一个左值。无论参数是什么类型,一旦给右值绑定了引用,右值也就有了名字,对于下一个调用函数而言,他也是左值。
如果要让func(T&& v)
按照左右值区分走不同的print
函数,就需要用到std::forward
。
将print
实现改为:template<typename T>
int func(T&& v)
{
print(std::forward<T>(v));
return v;
}
此刻输出就变成了预期的输出。这种使得参数传递不改变类型的方式就叫做完美转发。
gcc对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);
}
可见std::forward
使用static_cast
将类型映射到制定的模板类型上。执行func(a)
时,特化为func<int&>(int& &&)
,接着执行std::forward<int>(int &a)
,返回类型为int&
,调用print<int>(int&)
。当执行func(std::move(a))
时,特化为func<int&&>(int&& &&)
,接着执行std::forward<int&&>(a)
,返回类型为int&&
,调用print<int&&>(int&& &&)
。