33.当需要forward auto&&参数时,记得采用decltype

问题实例

 
C++14提供了一项新特性:允许开发者们在lambda中使用auto。实现它并不困难,无非就是把closure class的operator()变为了template而已。举例而言,对于下述lambda:

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

其生成的closure class的operator()大致有如下形式:
1
2
3
4
5
6
class SomeCompilerGeneratedClassName {
public:
template<typename T>
auto operator()(T x) const{ return func(normalize(x)); }

};

显然,lambda只负责将其参数x转发至normalize。如果normalize区分左右值的话,该lambda无法正确写入,原因在于参数x永远是一个左值,lambda永远将一个左值传递给normalize。

修改方法很容易:使用完美转发。这意味着x将成为一个universal reference,并且需要使用std::forward,不过这些都只是微不足道的小小修改:

1
auto f = [](auto&& x){ return func(normalize(std::forward<???>(x))); };

唯一存在疑问的地方在于,forward的模板参数应该是何种类型?

在通常情况下,完美转发存在于一个模板参数为T的模板函数中,因此我们在使用时只需要编写std::forward <T>即可,但lambda中不存在模板参数,因此我们写不了。


问题剖析与解决

 
众所周知,universal reference的性质由其初始化对象决定,若由左值初始化则变为左值引用,反之则为右值引用,因此我们可以通过判断x的类型来推断传递的参数是左值还是右值,此时decltype帮了大忙。如果传入的是左值,那么x必然是左值引用,反之则必为右值引用。但Item28亦曾提及,若传入左值,那么此时推衍类型T为左值引用,但如果传入右值,则推衍T为non-reference,这与decltype(x)将表现为右值引用有所背离。

如下是C++14中std::forward的实现:

1
2
3
4
template<typename T>
T&& forward(remove_reference_t<T>& param){
return static_cast<T&&>(param);
}

如果当前客户端需要forward一个右值Widget,此时forward将被实例化为:
1
2
3
Widget&& forward(Widget& param){
return static_cast<Widget&&>(param);
}

但如果当T被指定为Widget&&时,forward实例化如下所示:
1
2
3
Widget&& && forward(Widget& param){
return static_cast<Widget&& &&>(param);
}

执行引用塌缩之后,forward转为:
1
2
3
Widget&& forward(Widget& param){
return static_cast<Widget&&>(param);
}

显然,当我们将右值引用传入forward时,其效果与non-reference相同。这无疑是一个好消息,这意味着我们把decltype(x)的结果(右值引用)传递给forward时,其展现出的效果与non-reference一致,因此,完美转发可以被写为:
1
auto f =[](auto&& param){return func(normalize(std::forward<decltype(param)>(param)));};

如果当前需要转发的是一个参数集,那么在C++14中它可以被写为:
1
auto f =[](auto&&... params){return func(normalize(std::forward<decltype(params)>(params)...));};