Effective Modern C++ 条款33 对需要std::forward的auto&&参数使用decltype

对需要std::forward的auto&&参数使用decltype

泛型lambda(generic lambda)是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          // 关于auto返回类型看条款3
    { return func(normalize(x)); }

    ...              // 闭包类的其他功能
};

在这个例子中,lambda对x做的唯一的一件事就是把它转发给normalized。如果normalized区别对待左值和右值,这个lambda这样写是不合适的,因为即使传递给lambda的实参是个右值,lambda总是传一个左值(形参x)给normalized

写这个lambda的正确方式是把x完美转发给normalized,这样做需要在代码中修改两个地方。第一,x要改成通用引用(看条款24),第二,借助std::forward(看条款25)把x转发到normalized。在概念上,修改成这样:

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

但是,在概念和实现之间,有一个问题,就是你传递给std::forward的参数是什么类型,这决定了我上面写的???的地方会变成怎样。

一般来说,当你使用完美转发时,你是在一个接受类型形参T的模板函数中,所以你只是写std::forward<T>。而在泛型模板中,没有你可以拿到的类型参数T。在lambda生成的闭包内,模板化operator()函数有一个T,但是在lambda中无法指定它,所以它对你没有任何帮助。

条款28解释过如果把一个左值传递给通用引用,通用引用的类型会变为左值引用;如果把一个右值传递给通用引用,通用引用会变为一个右值引用。那意味着在我们的lambda中,我们可以通过检查x的类型来判断传递进来的实参是左值还是右值,decltype就可以让我们这样做。如果传递给lambda的是个左值,decltype(x)将会产生一个左值引用;如果传递给lambda的是个右值,decltype(x)将会产生一个右值引用。

条款28也解释了当使用std::forward时,有一个规则:传给它的类型参数是个左值引用时,表明返回的是个左值,传递给它的类型参数是个非引用类型时,表明返回的是个右值。在我们的lambda中,如果x绑定的是一个左值引用,decltype(x)将产生一个左值引用,这符合规则。不过,如果x绑定的是个右值,decltype(x)将会产生一个右值引用,而不是常规的非引用。(要注意的是,条款28中传递给std::forward的类型参数是T,而在lambda中无法使用T,也无法使用auto,所以只能将decltype(x)作为std::forward的类型参数。)

但看回在条款28中,std::forward的C++14实现:

template<typename T>     // 在命名空间std
T&& forward(remove_reference_t<T>& param)
{
    return static_cast<T&&>(param);
}

如果一个用户想要完美转发一个Widegt类型的右值,它正常地用Widget类型(即非引用类型)实例化std::forward,然后std::forward产生这个函数:

Widget&& forward(Widget& param)   // 当T为Widegt时,std::forward的实例化
{
    return static_cast<Widget&&>(param);
}

不过,请思考如果用户代码想要完美转发一个Widget类型的右值,但是这次它没有服从规则将T指定为非引用类型,而是将T指定为右值引用,这会发生什么?那是,思考把T替换成Widget&&将会发生什么。在std::forward实例化、应用了std::remove_reference_t之后,引用折叠(再次看条款28,看懂!)之前,std::forward看起来是这样的:

Widget&& && forward(Widget& param)   // 当T为Widget&&,std::forward的实例化
{                                    // 还没发生引用折叠
    return static_cast<Widget&& &&>(param);
}

如果你用这个实例化和把T设置为Widget的std::forward那个实例化进行比较,你会发现它们是相同的。那意味着用一个右值引用实例化std::forward,和用一个非引用类型实例化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)...));
    };

总结

需要记住的1点:

  • 对需要std::forwardauto&&参数使用decltype(Use decltype on auto&& parameters to std::forward them.)。
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页