【问题标题】:Why is template argument deduction disabled with std::forward?为什么用 std::forward 禁用模板参数推导?
【发布时间】:2011-10-15 18:46:37
【问题描述】:

在 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 似乎仅用于禁用模板参数推导。在这种情况下故意禁用它有什么意义?

【问题讨论】:

  • 请注意,当前std::forward 被声明为template&lt;typename T&gt; T&amp;&amp; forward(typename std::remove_reference&lt;T&gt;::type&amp; t); template&lt;typename T&gt; T&amp;&amp; forward(typename std::remove_reference&lt;T&gt;::type&amp;&amp; t);(并且都返回static_cast&lt;T&amp;&amp;&gt;(t))。 MSVC 正在关注forward 的早期版本。
  • 有趣。这与template&lt;typename T&gt; T&amp;&amp; forward(T&amp;&amp; t){ return static_cast&lt;T&amp;&amp;&gt;(t); } 有何不同?我在std::move 中看到了std::remove_reference 的用途,但这里没有。
  • 两个重载版本适用于右值,不需要std:identity,这实际上是完全来自标准的dropper。

标签: c++ visual-studio-2010 stl c++11


【解决方案1】:

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

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

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

示例:

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 的工作原理和方式。 :) +1
  • 请注意,只要删除remove_reference 就可以让一切正常运行——只要forward 接受&amp; 并且在调用它时保持明确的模板规范。需要明确的模板规范才能完成这项工作,因此remove_reference 确保我们不会忘记它。您的转发参考 (&amp;&amp;) 示例是红鲱鱼,恕我直言
  • “有名字的对象”严格来说是xvalues。 std::move(lvalue) 产生一个xvalue。
  • @rustyx,不是普通变量对象吗,还是不是 xvalues?
【解决方案2】:

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

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

std::forward(arg) 在语义上等同于arg。另一方面,std::forward&lt;Arg&gt;(arg) 在一般情况下不是空操作。

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


我认为如果我们关注 std::forward&lt;Arg&gt;(arg) 的确切作用,而不是 std::forward(arg) 会做什么(因为这是一个无趣的空操作),您会更好地理解事情。让我们尝试编写一个完美转发其参数的无操作函数模板。

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

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

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

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

在像noop 这样的完美转发函数的上下文中,有时我们想移动,但有时我们不想移动。知道我们是否应该移动的规则取决于Arg:如果它不是左值引用类型,则意味着noop 被传递了一个右值。如果它是左值引用类型,则意味着noop 被传递了一个左值。所以在std::forward&lt;NoopArg&gt;(arg) 中,NoopArg 是std::forward 的必要 参数,以便函数模板执行正确的操作。没有它,就没有足够的信息。这个NoopArg不与std::forward 的T 参数在一般情况下推导出的类型相同。

【讨论】:

  • 那么std::forward&lt;T&gt;(t) 与std::forward(t) 有何不同?
  • 我不明白如果它推断出模板参数类型而不是我明确给出它,为什么它会是一个空操作。你能详细说明/澄清一下吗?
  • @avakar 否。由于我们将变量传递给std::forward,因此T 将被推导出为左值引用类型,并且引用折叠规则将确保返回类型是相同的左值引用类型,因此调用将是一个左值。同样,std::forward(0) 将是一个 xvalue。因此,推导 std::forward 将完美地转发它的论点并且是无操作的。
  • @Dave 对。但是t 没有用作右值引用,我不确定你为什么提到这一点。你的意思是绑定std::forward的参数吗?你知道template&lt;typename T&gt; void foo(T&amp;&amp;); 然后T&amp;&amp; 可能是也可能不是右值引用吗?
  • @hvd 函数模板无法区分纯右值和 xvalue。最后的绑定参数是int&amp;&amp; 类型的“值”,它也是返回的“值”。因此,无操作。
【解决方案3】:

简答:

因为std::forward 按预期工作(即忠实地传递原始类型信息),它意味着被使用 INSIDE TEMPLATE CONTEXT,并且它必须使用从封闭模板上下文推导出的类型参数,而不是推导类型参数本身(因为只有封闭的模板才有机会推断出真正的类型信息,这将在细节中解释),因此必须提供类型参数。

虽然在非模板上下文中使用std::forward 是可能的,但它毫无意义(将在详细说明中解释)。

如果有人敢尝试实现std::forward来进行类型推断,那他/她注定会痛苦地失败。

详情:

例子:

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

观察arg被声明为T&amp;&amp;,(它是推断传递的真实类型的关键,并且)它不是右值引用,虽然它具有相同的语法,但它被称为通用引用 (Terminology coined by Scott Meyers),因为T 是泛型类型,(同样,string s; auto &amp;&amp; ss = s; 中的 ss 不是右值引用)。

感谢通用参考,在实例化someFunc 时会发生一些类型推断魔法,具体如下:

  • 如果将类型为_T 或_T &amp; 的右值对象传递给someFunc,则T 将被推导出为_T &amp;(,是的,即使X 的类型是只是_T,请阅读Meyers' artical);
  • 如果将_T &amp;&amp; 类型的右值传递给someFunc,则T 将推导出为_T &amp;&amp;

现在,您可以将T 替换为上述代码中的真实类型:

当传递左值obj时:

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

在应用引用折叠规则(请阅读Meyers' artical)后,我们得到:

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)); }

在应用参考折叠规则后(请阅读Meyers' artical),我们得到:

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

现在,你可以猜到 std::forwrd 本质上所做的只是static_cast&lt;T&gt;(para)(,实际上,在clang 11 的实现中它是static_cast&lt;T &amp;&amp;&gt;(para),在应用引用折叠之后是一样的规则)。一切正常。

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

如果你试图让编译器这样做,它永远不会被推断为_T &amp;&amp;(,是的,即使当arg绑定到_T &amp;&amp;时 strong>,它仍然是someFunc内的一个lvaule obj,因此只能推断为_T或_T &amp;....你真的应该阅读Meyers' artical)。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-10-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多