0%

universal reference与完美转发

引子

声明一个模板函数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的场景必须是不能确定引用类型的,变量的实际类型需要进行类型推导得出:

  1. 模板函数中参数为T&&
  2. 使用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)中的参数vprint看起来也是一个左值。无论参数是什么类型,一旦给右值绑定了引用,右值也就有了名字,对于下一个调用函数而言,他也是左值。

如果要让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&& &&)

参考资料

Disqus评论区没有正常加载,请使用科学上网