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

发布于 2024-12-10 06:24:58 字数 326 浏览 0 评论 0原文

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

In VS2010 std::forward is defined as such:

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

identity appears to be used solely to disable template argument deduction. What's the point of purposefully disabling it in this case?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

私藏温柔 2024-12-17 06:24:58

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

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

因为具有名称的对象是左值,这是唯一一次std::forward7func())时, > 将正确转换为 T&& 。在完美转发的情况下,传递给 std::forward 的 arg 是一个左值,因为它有一个名称。 std::forward 的类型将被推导为左值引用或 const 左值引用。引用折叠规则会导致 std::forward 中 static_cast(arg) 中的 T&& 始终解析为左值引用或 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));
}

If you pass an rvalue reference to an object of type X to a template function that takes type T&& as its parameter, template argument deduction deduces T to be X. Therefore, the parameter has type X&&. If the function argument is an lvalue or const lvalue, the compiler deduces its type to be an lvalue reference or const lvalue reference of that type.

If std::forward used template argument deduction:

Since objects with names are lvalues the only time std::forward would correctly cast to T&& would be when the input argument was an unnamed rvalue (like 7 or func()). In the case of perfect forwarding the arg you pass to std::forward is an lvalue because it has a name. std::forward's type would be deduced as an lvalue reference or const lvalue reference. Reference collapsing rules would cause the T&& in static_cast<T&&>(arg) in std::forward to always resolve as an lvalue reference or const lvalue reference.

Example:

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));
}
别把无礼当个性 2024-12-17 06:24:58

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

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

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

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


我认为,如果我们专注于 std::forward(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&< /code>),因此返回类型为 int&,这次我们无法从表达式 std::move(arg) 绑定这样的左值引用是一个 x 值。

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

Because std::forward(expr) is not useful. The only thing it can do is a no-op, i.e. perfectly-forward its argument and act like an identity function. The alternative would be that it's the same as std::move, but we already have that. In other words, assuming it were possible, in

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

std::forward(arg) is semantically equivalent to arg. On the other hand, std::forward<Arg>(arg) is not a no-op in the general case.

So by forbidding std::forward(arg) it helps catch programmer errors and we lose nothing since any possible use of std::forward(arg) are trivially replaced by arg.


I think you'd understand things better if we focus on what exactly std::forward<Arg>(arg) does, rather than what std::forward(arg) would do (since it's an uninteresting no-op). Let's try to write a no-op function template that perfectly forwards its argument.

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

This naive first attempt isn't quite valid. If we call noop(0) then NoopArg is deduced as int. This means that the return type is int&& and we can't bind such an rvalue reference from the expression arg, which is an lvalue (it's the name of a parameter). If we then attempt:

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

then int i = 0; noop(i); fails. This time, NoopArg is deduced as int& (reference collapsing rules guarantees that int& && collapses to int&), hence the return type is int&, and this time we can't bind such an lvalue reference from the expression std::move(arg) which is an xvalue.

In the context of a perfect-forwarding function like noop, sometimes we want to move, but other times we don't. The rule to know whether we should move depends on Arg: if it's not an lvalue reference type, it means noop was passed an rvalue. If it is an lvalue reference type, it means noop was passed an lvalue. So in std::forward<NoopArg>(arg), NoopArg is a necessary argument to std::forward in order for the function template to do the right thing. Without it, there's not enough information. This NoopArg is not the same type as what the T parameter of std::forward would be deduced in the general case.

情绪失控 2024-12-17 06:24:58

简短回答:

因为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 中,ss 不是右值 参考)。

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

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

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

当传递左值 obj 时:

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

并且在应用引用折叠规则(后,请阅读Meyers 的文章),我们得到

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 的文章),我们得到:

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

现在,你可以猜到 std::forwrd 本质上所做的只是 static_cast(para)( 事实上,在 clang 中11 的实现是static_cast(para),应用引用折叠规则后是一样的)。一切都很顺利。

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

如果您尝试让编译器执行此操作,则它永远不会被推导为_T &&(,是的,甚至arg 绑定到 _T &&,它仍然是 someFunc 内的 lvaule obj,因此只能是推论为_T_T &....你真的应该阅读Meyers 的文章)。

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

Short answer:

Because for std::forward to work as intended(, i.e. to faitfully pass the original type info), it is meant to be used INSIDE TEMPLATE CONTEXT, and it must use the deduced type param from the enclosing template context, instead of deducing the type param by itself(, since only the enclosing templates have the chance to deduce the true type info, this will be explained in the details), hence the type param must be provided.

Though using std::forward inside non-template context is possible, it is pointless(, will be explained in the details).

And if anyone dares to try implementing std::forward to allow type deducing, he/she is doomed to fail painfully.

Details:

Example:

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

Observer that arg is declared as T&&,( it is the key to deduce the true type passed, and) it is not a rvalue reference, though it has the same syntax, it is called an universal reference (Terminology coined by Scott Meyers), because T is a generic type, (likewise, in string s; auto && ss = s; ss is not a rvalue reference).

Thanks to universal reference, some type deduce magic happens when someFunc is being instantiated, specifically as following:

  • If an rvalue object, which has the type _T or _T &, is passed to someFunc, T will be deduced as _T &(, yeah, even if the type of X is just _T, please read Meyers' artical);
  • If an rvalue of type _T && is passed to someFuncT will be deduced as _T &&

Now, you can replace T with the true type in above code:

When lvalue obj is passed:

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

And after applying reference collapse rule(, pls read Meyers' artical), we get:

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

When rvalue obj is passed:

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

And after applying reference collapse rule(, pls read Meyers' artical), we get:

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

Now, you can guess what std::forwrd does eseentially is just static_cast<T>(para)(, in fact, in clang 11's implementation it is static_cast<T &&>(para), which is the same after applying reference collapsing rule). Everything works out fine.

But if you think about let std::fowrd deducing the type param by itself, you'll quickly find out that inside someFunc, std::forward literally IS NOT ABLE TO deduce the original type of arg.

If you try to make the compiler do it, it will never be deduced as _T &&(, yeah, even when arg is bind to an _T &&, it is still an lvaule obj inside someFunc, hence can only be deduceed as _T or _T &.... you really should read Meyers' artical).

Last, why should you only use std::forward inside templates? Because in non-templates context, you know exactly what type of obj you have. So, if you have an lvalue bind to an rvalue reference, and you need to pass it as an lvaule to another function, just pass it, or if you need to pass it as rvalue, just do std::move. You simply DON'T NEED std::forward inside non-template context.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文