EffectiveModernCppChinese/6.LambdaExpressions/item33.md
2021-01-21 11:24:36 +08:00

5.2 KiB
Raw Blame History

对于std::forward的auto&&形参使用decltype

泛型lambda(generic lambdas)是C++14中最值得期待的特性之一——因为在lambda的参数中可以使用auto关键字。这个特性的实现是非常直截了当的即在闭包类中的operator()函数是一个函数模版。例如存在这么一个lambda

auto f = [](auto x){ return func(normalize(x)); };

对应的闭包类中的函数调用操作符看来就变成这样:

class SomeCompilerGeneratedClassName { public:
  template<typename T>
  auto operator()(T x) const
  { return func(normalize(x)); }
  ...
};

在这个样例中lambda对变量x做的唯一一件事就是把它转发给函数normalize。如果函数normalize对待左值右值的方式不一样这个lambda的实现方式就不大合适了因为即使传递到lambda的实参是一个右值lambda传递进去的形参总是一个左值。

实现这个lambda的正确方式是把x完美转发给函数normalize。这样做需要对代码做两处修改。首先x需要改成通用引用其次需要使用std::forwardx转发到函数normalize。实际上的修改如下:

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就可以实现这样的效果。传递给lambda的是一个左值decltype(x)就能产生一个左值引用;如果传递的是一个右值,decltype(x)就会产生右值引用。

Item28也解释过在调用std::forward传递给它的类型类型参数是一个左值引用时会返回一个左值传递的是一个非引用类型时返回的是一个右值引用而不是常规的非引用。在前面的lambda中如果x绑定的是一个左值引用decltype(x)就能产生一个左值引用;如果绑定的是一个右值,decltype(x)就会产生右值引用,而不是常规的非引用。

在看一下Item28中关于std::forward的C++14实现

template<typename T> // in namespace 
T&& forward(remove_reference_t<T>& param) // std
{
	return static_cast<T&&>(param); 
}

如果用户想要完美转发一个Widget类型的右值时它会使用Widget类型非引用类型来示例化std::forward,然后产生以下的函数:

Widget&& forward(Widget& param) 
{ 																		 // instantiation of 
	return static_cast<Widget&&>(param); // std::forward when 
} 																		 // T is Widget

思考一下如果用户代码想要完美转发一个Widget类型的右值但没有遵守规则将T指定为非引用类型而是将T指定为右值引用这回发生什么思考将T换成Widget如何,在std::forward实例化、应用了remove_reference_t后,音乐折叠之前,这是产生的代码:

Widget&& && forward(Widget& param)        // instantiation of 
{ 																        // std::forward when 
	return static_cast<Widget&& &&>(param); // T is Widget&&
} 																			  // (before reference-collapsing)

应用了引用折叠之后,代码会变成:

Widget&& forward(Widget& param)        // instantiation of 
{                                         // std::forward when 
	return static_cast<Widget&&>(param); // T is Widget&&
}                                         // (before reference-collapsing)

对比发现,用一个右值引用去实例化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(pram)>(param)));
    };

再加上6个点就可以让我们的lambda完美转发接受多个参数了因为C++14中的lambda参数是可变的

auto f = 
    [](auto&&... params)
    {
        return 
          func(normalized(std::forward<decltype(params)>(params)...));
    };

要谨记的是

  • auto&&参数使用decltype来(std::forward)转发参数;