条款三十三:对auto&&
形参使用decltype
以std::forward
它们
Item 33: Use decltype
on auto&&
parameters to std::forward
them
泛型lambda(generic lambdas)是C++14中最值得期待的特性之一——因为在lambda的形参中可以使用auto
关键字。这个特性的实现是非常直截了当的:即在闭包类中的operator()
函数是一个函数模版。例如存在这么一个lambda,
auto f = [](auto x){ return func(normalize(x)); };
对应的闭包类中的函数调用操作符看来就变成这样:
class SomeCompilerGeneratedClassName {
public:
template<typename T> //auto返回类型见条款3
auto operator()(T x) const
{ return func(normalize(x)); }
… //其他闭包类功能
};
在这个样例中,lambda对变量x
做的唯一一件事就是把它转发给函数normalize
。如果函数normalize
对待左值右值的方式不一样,这个lambda的实现方式就不大合适了,因为即使传递到lambda的实参是一个右值,lambda传递进normalize
的总是一个左值(形参x
)。
实现这个lambda的正确方式是把x
完美转发给函数normalize
。这样做需要对代码做两处修改。首先,x
需要改成通用引用(见Item24),其次,需要使用std::forward
将x
转发到函数normalize
(见Item25)。理论上,这都是小改动:
auto f = [](auto&& x)
{ return func(normalize(std::forward<???>(x))); };
在理论和实际之间存在一个问题:你应该传递给std::forward
的什么类型,即确定我在上面写的???
该是什么。
一般来说,当你在使用完美转发时,你是在一个接受类型参数为T
的模版函数里,所以你可以写std::forward<T>
。但在泛型lambda中,没有可用的类型参数T
。在lambda生成的闭包里,模版化的operator()
函数中的确有一个T
,但在lambda里却无法直接使用它,所以也没什么用。
Item28解释过如果一个左值实参被传给通用引用的形参,那么形参类型会变成左值引用。传递的是右值,形参就会变成右值引用。这意味着在这个lambda中,可以通过检查形参x
的类型来确定传递进来的实参是一个左值还是右值,decltype
就可以实现这样的效果(见Item3)。传递给lambda的是一个左值,decltype(x)
就能产生一个左值引用;如果传递的是一个右值,decltype(x)
就会产生右值引用。
Item28也解释过在调用std::forward
时,惯例决定了类型实参是左值引用时来表明要传进左值,类型实参是非引用就表明要传进右值。在前面的lambda中,如果x
绑定的是一个左值,decltype(x)
就能产生一个左值引用。这符合惯例。然而如果x
绑定的是一个右值,decltype(x)
就会产生右值引用,而不是常规的非引用。
再看一下Item28中关于std::forward
的C++14实现:
template<typename T> //在std命名空间
T&& forward(remove_reference_t<T>& param)
{
return static_cast<T&&>(param);
}
如果用户想要完美转发一个Widget
类型的右值时,它会使用Widget
类型(即非引用类型)来实例化std::forward
,然后产生以下的函数:
Widget&& forward(Widget& param) //当T是Widget时的std::forward实例
{
return static_cast<Widget&&>(param);
}
思考一下如果用户代码想要完美转发一个Widget
类型的右值,但没有遵守规则将T
指定为非引用类型,而是将T
指定为右值引用,这会发生什么。也就是,思考将T
换成Widget&&
会如何。在std::forward
实例化、应用了std::remove_reference_t
后,引用折叠之前,std::forward
看起来像这样:
Widget&& && forward(Widget& param) //当T是Widget&&时的std::forward实例
{ //(引用折叠之前)
return static_cast<Widget&& &&>(param);
}
应用了引用折叠之后(右值引用的右值引用变成单个右值引用),代码会变成:
Widget&& forward(Widget& param) //当T是Widget&&时的std::forward实例
{ //(引用折叠之后)
return static_cast<Widget&&>(param);
}
对比这个实例和用Widget
设置T
去实例化产生的结果,它们完全相同。表明用右值引用类型和用非引用类型去初始化std::forward
产生的相同的结果。
那是一个很好的消息,因为当传递给lambda形参x
的是一个右值实参时,decltype(x)
可以产生一个右值引用。前面已经确认过,把一个左值传给lambda时,decltype(x)
会产生一个可以传给std::forward
的常规类型。而现在也验证了对于右值,把decltype(x)
产生的类型传递给std::forward
是非传统的,不过它产生的实例化结果与传统类型相同。所以无论是左值还是右值,把decltype(x)
传递给std::forward
都能得到我们想要的结果,因此lambda的完美转发可以写成:
auto f =
[](auto&& param)
{
return
func(normalize(std::forward<decltype(param)>(param)));
};
再加上6个点,就可以让我们的lambda完美转发接受多个形参了,因为C++14中的lambda也可以是可变形参的:
auto f =
[](auto&&... params)
{
return
func(normalize(std::forward<decltype(params)>(params)...));
};
请记住:
- 对
auto&&
形参使用decltype
以std::forward
它们。