为什么模板参数推导被禁用 std::forward

Why is template argument deduction disabled with std::forward?

本文关键字:std forward 参数 为什么      更新时间:2023-10-16

在VS2010中,std::forward被定义为:

template<class _Ty> inline
_Ty&& forward(typename identity<_Ty>::type& _Arg)
{   // forward _Arg, given explicitly specified type parameter
    return ((_Ty&&)_Arg);
}

identity似乎仅用于禁用模板参数推断。在这种情况下故意禁用它有什么意义?

如果将对类型

X 对象的右值引用传递给以类型 T&& 作为其参数的模板函数,则模板参数推导会推断出T X。因此,该参数的类型为 X&& 。如果函数参数是左值或常量左值,编译器会将其类型推断为该类型的左值引用或常量左值引用。

如果std::forward使用了模板参数推导:

由于objects with names are lvalues std::forward唯一正确转换为T&&的时间是当输入参数是未命名的右值(如7func()(时。在完美转发的情况下,您传递给std::forward arg是一个左值,因为它有一个名称。 std::forward的类型将被推导出为右值引用或常量左值引用。引用折叠规则将导致 std::forward 中static_cast<T&&>(arg)中的T&&始终解析为左值引用或常量左值引用。

例:

template<typename T>
T&& forward_with_deduction(T&& obj)
{
    return static_cast<T&&>(obj);
}
void test(int&){}
void test(const int&){}
void test(int&&){}
template<typename T>
void perfect_forwarder(T&& obj)
{
    test(forward_with_deduction(obj));
}
int main()
{
    int x;
    const int& y(x);
    int&& z = std::move(x);
    test(forward_with_deduction(7));    //  7 is an int&&, correctly calls test(int&&)
    test(forward_with_deduction(z));    //  z is treated as an int&, calls test(int&)
    //  All the below call test(int&) or test(const int&) because in perfect_forwarder 'obj' is treated as
    //  an int& or const int& (because it is named) so T in forward_with_deduction is deduced as int& 
    //  or const int&. The T&& in static_cast<T&&>(obj) then collapses to int& or const int& - which is not what 
    //  we want in the bottom two cases.
    perfect_forwarder(x);           
    perfect_forwarder(y);           
    perfect_forwarder(std::move(x));
    perfect_forwarder(std::move(y));
}

因为std::forward(expr)没有用。它唯一能做的就是无操作,即完美地转发其参数并像恒等函数一样行事。另一种选择是它与 std::move 相同,但我们已经有了。换句话说,假设这是可能的,在

template<typename Arg>
void generic_program(Arg&& arg)
{
    std::forward(arg);
}

std::forward(arg)在语义上等同于arg。另一方面,在一般情况下,std::forward<Arg>(arg)不是无操作。

因此,通过禁止std::forward(arg)它有助于捕获程序员的错误,我们没有任何损失,因为任何可能的std::forward(arg)使用都被arg轻松取代。


我认为如果我们专注于std::forward<Arg>(arg)到底做了什么,而不是std::forward(arg)会做什么(因为这是一个无趣的禁操作(,你会更好地理解事情。让我们尝试编写一个完美地转发其参数的无操作函数模板。

template<typename NoopArg>
NoopArg&& noop(NoopArg&& arg)
{ return arg; }

这种幼稚的第一次尝试并不完全有效。如果我们称noop(0)那么NoopArg被推导出为int 。这意味着返回类型是int&&的,我们不能从表达式 arg 绑定这样的右值引用,这是一个左值(它是一个参数的名称(。如果我们随后尝试:

template<typename NoopArg>
NoopArg&& noop(NoopArg&& arg)
{ return std::move(arg); }

然后int i = 0; noop(i);失败。这一次,NoopArg被推导为int&(引用折叠规则保证int& &&折叠为int&(,因此返回类型为int&,这次我们不能从表达式std::move(arg)绑定这样的左值引用,这是一个xvalue。

在像这样的完美转发函数的上下文中 noop有时我们想移动,但有时我们不想。知道我们是否应该移动的规则取决于Arg:如果它不是左值引用类型,则意味着noop传递了一个右值。如果是左值引用类型,则表示noop传递了左值。所以在std::forward<NoopArg>(arg)中,NoopArg是为了使函数模板做正确的事情而std::forward的必要参数。没有它,就没有足够的信息。此NoopArg与一般情况下推导std::forwardT参数的类型不同

简短回答:

因为要使std::forward预期工作(,即轻松传递原始类型信息(,它应该模板上下文中使用,并且它必须使用从封闭模板上下文推导出的类型参数而不是自己推断类型参数(,由于只有封闭模板才有机会推断出真实的类型信息,这将在细节中解释(,因此必须提供类型参数。

尽管可以在非模板上下文中使用std::forward,但它毫无意义(将在详细信息中解释(。

如果有人敢于尝试实现std::forward以允许类型推断,他/她注定要痛苦地失败。

详:

例:

template <typename T>
auto someFunc(T&& arg){ doSomething(); call_other_func(std::forward<T>(para)); }

观察者认为arg被声明为 T&& ,(它是推断传递的真实类型的关键,并且(它不是右值引用,尽管它具有相同的语法,但它被称为通用引用(Scott Meyers 创造的术语(,因为T是泛型类型,(同样,在string s; auto && ss = s; ss 不是右值引用(。

由于通用引用,当实例化someFunc时,某些类型推导出魔术会发生,具体如下:

  • 如果将类型为 _T_T & 的 rvalue 对象传递给 someFuncT 将被推导出为 _T &(是的,即使X的类型只是_T,请阅读 Meyers 的文章(;
  • 如果将类型为 _T && 的右值传递给 someFuncT将被推导出为 _T &&

现在,您可以将T替换为上面代码中的 true 类型:

当传递左值 obj 时:

auto someFunc(_T & && arg){ doSomething(); call_other_func(std::forward<_T &>(arg)); }

在应用引用折叠规则(,请阅读迈耶斯的文章(后,我们得到:

auto someFunc(_T & arg){ doSomething(); call_other_func(std::forward<_T &>(arg)); }

当传递右值 obj 时:

auto someFunc(_T && && arg){ doSomething(); call_other_func(std::forward<_T &&>(arg)); }

在应用引用折叠规则(,请阅读迈耶斯的文章(后,我们得到:

auto someFunc(_T && arg){ doSomething(); call_other_func(std::forward<_T &&>(arg)); }

现在,你可以猜到std::forward所做的只是static_cast<T>(para)(实际上,在clang 11的实现中它是static_cast<T &&>(para)的,在应用引用折叠规则后是一样的(。一切都很好。

但是如果你考虑让 std::fowrd 自己推导出类型参数,你很快就会发现在 someFunc 内部 std::forward字面上无法推导出原始类型的arg

如果你试图让编译器这样做,它永远不会被推导出为_T &&(,是的,即使arg绑定到一个_T &&,它仍然是someFunc内部的 lvaule obj,因此只能推断为_T_T &......你真的应该阅读迈耶斯的文章(。

最后,为什么只在模板中使用std::forward?因为在非模板上下文中,您确切地知道您拥有哪种类型的 obj。因此,如果您将绑定右值引用,并且需要将其作为 lvaule 传递给另一个函数,只需传递它,或者如果您需要将其作为右值传递只需执行std::move。你根本不需要在非模板上下文中输入 std::forward