1. 你有完美转发lambda入参的需求么?
泛型lambda式(generic lambda
)是C++14最振奋人心的特征之一————lambda可以在形参规格中使用auto
。这个特性的实现十分直接了当:闭包类中的operator()
采用模板实现。例如,给定下述lambda式:
auto f = [](auto x) { return func(normalize(x));};
则闭包类的函数调用运算符如下所示:
class SomeComilerGeneratedClassName {
public:
template<typename T> //auto型别的返回值
auto operator()(T x) const //参见Item 3
{ return func(normalize(x));}
...
}; //闭包类的其他功能
传送门:Item 3
在本例中,lambda式对x
实施的位移动作就是将其转发给normalize
。如果normalize
区别对待左值和右值,则可说该lambda式撰写的是有问题的,因为,lambda总会传递左值(形参x)给normalize
,即使传递给lambda式的实参是个右值。
该lambda式的正确撰写方式是把x
完美转发给normalize
,这就要求代码中修改两处,首先,x
要改成万能引用(参见Item 24);其次,使用std::forward
(参见Item 25)把x转发给normalize
。概念上不难理解,这两处的修改都是举手之劳:
auto f = [](auto&& x){ return func(normalize(std::forward<???>(x)));};
可是问题就出在,上面代码里面的???
应该怎么办呢?
2. 怎么完美转发lambda函数的入参
通常情况下,在使用完美转发的时候,你是在一个接受型别形参T的模板函数中,所以,你写std::forward<T>
就好。但是在泛型lambda式中,却没有可用的型别形参T。在lambda式生成的闭包内的模板化operator()
函数中的确有个T,但是在lambda式中无法指涉,所以也么用。
Item 28解释过,如果把左值传递给万能引用的形参,则该形参的型别会作为左值引用,如果传递是右值,则该形参会成为右值引用。那意味着在我们的lambda式中,我们可以通过探查x
的型别,来判断传入的实参是左值还是右值。
decltype(x)
将会产生左值引用型别,如果传入的是个右值,decltype(x)
将会产生右值引用。
Item 28还解释了,使用std::forward
时惯例是:用型别形参为左值引用表明想要返回左值,而用非引用型别时来表达想要返回的右值。
再看看我们的lambda式,如果x
绑定了左值,decltype(x)
将产生左值引用型别。这符合惯例。不过,如果x
绑定的是个右值,decltype(x)
将会产生右值引用惯例,而非符合惯例的非引用。
3. 模板实例化分析
但是,再看下Item 28中的std::forward
的C++14实现:
template<typename T> //在名字空间std中
T&& forward(remove_reference_t<T>& param)
{
return static_cast<T&&>(param);
}
如果客户代码欲完美转发Widget型别的右值,按照惯例,它应该采用Widget型别(即非引用型别)来实例化std::forward
,然后std::forward
模板会产生如下函数:
Widget&& forward(Widget& param) //T取Widget时
{
return static_cast<Widget&&>(param); //std::forward的实例化结束
}
但是,如果客户代码想要完美转发Widget
的同一右值,但是这次没有遵从惯例将T指定为非引用型别,而是将T指定为右值引用,这会导致什么结果?这就是需要思考的问题,T指定为Widget&&
将会发生什么事情。在std::forward
完成初步的实例化并实施了std::remove_reference_t
之后,但在引用折叠(再参见Item 28)发生之前,std::forward
如下所示:
Widget&& && forward(Widget& param) //T取Widget&&时
{
return static_cast<Widget&& &&>(param); //std::forward的实例化结束
//(在引用折叠发生之前)
}
然后,发生引用折叠,右值引用的右值引用结果是单个右值引用,实例化结果为:
Widget&& forward(Widget& param) //T取Widget&&时
{
return static_cast<Widget&&>(param); //std::forward的实例化结束
//(在引用折叠发生之后)
}
对比之前,发现一样的。这意味着,实例化std::forward
时,使用一个右值引用型别和使用一个非引用型别,会产生相同结果。
4. 结论
这个结果非常不错,因为如果传递给lambda式的形参x
是个右值,decltype(x)
产生的是右值引用型别,我们之前已经知道了,传递左值给我们的lambda式时,decltype(x)
会产生传递给std::forward
符合惯例的型别,而现在,我们又知道对右值而言,虽然decltype(x)
产生的型别并不符合传递给std::forward
的型别形参的管理。但是产生的结果殊途同归。所以,这里无论左值右值都能够符合要求。
所以我们可以这么写:
//单一变量
auto f =
[](auto&& param)
{
return func(normalizer(std::forward<decltype(param)>(param)));
}
//多变量
auto f =
[](auto&&... param)
{
return func(normalizer(std::forward<decltype(param)>(param)...));
}
要点速记 |
---|
1. 对auto&&型别的形参使用decltype,以std::forward之。 |