【问题标题】:Why is std::forward necessary with forwarding references [duplicate]为什么转发引用需要 std::forward [重复]
【发布时间】:2017-07-08 08:12:34
【问题描述】:

在这样的函数模板中

template <typename T>
void foo(T&& x) {
  bar(std::forward<T>(x));
}

如果foo 使用右值引用调用,x 不是在foo 中的右值引用吗?如果使用左值引用调用 foo,则无论如何都不需要强制转换,因为x 也将是foo 内的左值引用。同样T 会被推导出为左值引用类型,所以std::forward&lt;T&gt; 不会改变x 的类型。

我使用boost::typeindex 进行了测试,我得到的类型完全相同,有无std::forward&lt;T&gt;

#include <iostream>
#include <utility>

#include <boost/type_index.hpp>

using std::cout;
using std::endl;

template <typename T> struct __ { };

template <typename T> struct prt_type { };
template <typename T>
std::ostream& operator<<(std::ostream& os, prt_type<T>) {
  os << "\033[1;35m" << boost::typeindex::type_id<T>().pretty_name()
     << "\033[0m";
  return os;
}

template <typename T>
void foo(T&& x) {
  cout << prt_type<__<T>>{} << endl;
  cout << prt_type<__<decltype(x)>>{} << endl;
  cout << prt_type<__<decltype(std::forward<T>(x))>>{} << endl;
  cout << endl;
}

int main(int argc, char* argv[])
{
  foo(1);

  int i = 2;
  foo (i);

  const int j = 3;
  foo(j);

  foo(std::move(i));

  return 0;
}

g++ -Wall test.cc &amp;&amp; ./a.outgcc 6.2.0boost 1.62.0 的输出是

__<int>
__<int&&>
__<int&&>

__<int&>
__<int&>
__<int&>

__<int const&>
__<int const&>
__<int const&>

__<int>
__<int&&>
__<int&&>

编辑:我找到了这个答案:https://stackoverflow.com/a/27409428/2640636 显然,

只要你给参数一个名字,它就是一个左值。

我的问题是,为什么选择这种行为而不是保留右值引用作为右值,即使它们被赋予名称?在我看来,整个转发难题都可以通过这种方式规避。

Edit2:我不是在问std::forward什么。我在问为什么需要它。

【问题讨论】:

    标签: c++ c++11 move-semantics perfect-forwarding forwarding-reference


    【解决方案1】:

    x 不是 foo 内部的右值引用吗

    不,xfoo 中的一个左值(它有一个名称和一个地址),类型为右值引用。结合参考折叠规则和模板类型推导规则,您会发现您需要std::forward 才能获得正确的参考类型。

    基本上,如果你作为x 传递给的是一个左值,比如int,那么T 会被推导出为int&amp;。然后int &amp;&amp; &amp; 变为int&amp;(由于引用折叠规则),即左值引用。

    另一方面,如果你传递一个右值,比如42,那么T被推导出为int,所以最后你有一个int&amp;&amp;作为x的类型,即a右值。基本上这就是 std::forward 所做的:将结果转换为 T&amp;&amp;,就像

    static_cast<T&&>(x)
    

    由于引用折叠规则,它变为T&amp;&amp;T&amp;

    它的用处在泛型代码中变得显而易见,您可能事先不知道您将获得右值还是左值。如果您不调用std::forward 而只调用f(x),那么x始终是左值,因此您将在需要时丢失移动语义,最终可能会出现 un-必要的副本等。

    您可以看到差异的简单示例:

    #include <iostream>
    
    struct X
    {
        X() = default;
        X(X&&) {std::cout << "Moving...\n";};
        X(const X&) {std::cout << "Copying...\n";}
    };
    
    template <typename T>
    void f1(T&& x)
    {
        g(std::forward<T>(x));
    }
    
    template <typename T>
    void f2(T&& x)
    {
        g(x);
    }
    
    template <typename T>
    void g(T x)
    { }
    
    int main()
    {
        X x;
        std::cout << "with std::forward\n";
        f1(X{}); // moving
    
        std::cout << "without std::forward\n";
        f2(X{}); // copying
    }
    

    Live on Coliru

    【讨论】:

    • 我明白std::forward 是什么意思。我不明白为什么需要这样做。就像你说的,如果你传递一个右值,比如int&amp;&amp;,那么int&amp;&amp; &amp;&amp; 折叠成int&amp;&amp;,但是int&amp;&amp; 是我们不想要的,所以,不是我们好去吗?
    • 问题在于,在通用代码中,您可能事先不知道您传递的x 是右值还是左值。如果您只是说f(x),那么x 将始终被视为左值。
    • @SU3 std::move 将始终转换为右值,而 std::forward 将仅有条件地转换为右值。所以why 是因为有时必须完成这种转换为右值的操作。另外 - 请记住,所有函数参数始终是左值 - 那是因为它们有一个名称。
    【解决方案2】:

    真的不希望你的参数被自动移动到调用的函数中。考虑这个函数:

    template <typename T>
    void foo(T&& x) {
      bar(x);
      baz(x);
      global::y = std::forward<T>(x);
    }
    

    现在你真的想要自动移动到bar 和一个空参数到baz

    要求您指定是否以及何时移动或转发参数的当前规则并非偶然。

    【讨论】:

    • 人们真的会更频繁地编写这种代码吗?在同一个函数中的右值引用在移动之前会发生什么事情?我只在物体移动后对其进行过操作。所以,这对我来说似乎是一个例外情况。
    • @vsoftco 在他已删除的评论中指出,在语言中以相反的方式处理将需要 anti_forward。我想我更喜欢那个。有没有这种模式无法处理的情况?在您的示例中,您可以在之后调用global::y 上的barbaz
    • @SU3 如果某件事发生的概率不为零,那么它肯定会在某个程序中的某个时间发生。这将是一个难以发现的错误。委员会可能认为(我也有同样的观点)最好是复制而不是意外移动。 std::forward 已经有点复杂了,想象一下有一个 anti-std::forward...人们会发疯的 :)
    • 这个转发引用还有另一种情况,T 实际上是一些const U&amp;,而x 根本不是右值。使用std::forward&lt;T&gt;(x) 将适用于这两种情况。正如我在之前的评论中所说,在说明T&amp;&amp; 的规则之前,已经真正彻底地考虑了这一点
    • @BoPersson,我没想到这没有真正彻底考虑。我只是想知道考虑了什么。现在,在您和 vsoftco 的帮助下,我想我明白了。
    【解决方案3】:

    我得到完全相同的类型有和没有std::forward&lt;T&gt;

    ...不是吗?你自己的输出证明你错了:

    __<int>    // T
    __<int&&>  // decltype(x)
    __<int&&>  // std::forward<T>(x)
    

    如果不使用std::forward&lt;T&gt;decltype(x),您将获得int 而不是int&amp;&amp;。这可能会不经意地“传播x 的右值性” - 考虑这个例子:

    void foo(int&)  { cout << "int&\n"; }
    void foo(int&&) { cout << "int&&\n"; }
    
    template <typename T>
    void without_forward(T&& x)
    {
        foo(x);
    //      ^
    //  `x` is an lvalue!
    }
    
    template <typename T>
    void with_forward(T&& x)
    {
    //  `std::forward` casts `x` to `int&&`.
    //      vvvvvvvvvvvvvvvvvv
        foo(std::forward<T>(x));
    //                      ^
    //          `x` is an lvalue!
    }
    
    template <typename T>
    void with_decltype_cast(T&& x)
    {
    // `decltype(x)` is `int&&`. `x` is casted to `int&&`.
    //      vvvvvvvvvvv
        foo(decltype(x)(x));
    //                  ^
    //          `x` is an lvalue!
    }
    
    int main()
    {
        without_forward(1);    // prints "int&"
        with_forward(1);       // prints "int&&"
        with_decltype_cast(1); // prints "int&&"
    }
    

    wandbox example

    【讨论】:

    • 我不明白。我的输出如何证明我错了? int 是我得到的 Tx 的类型不一定与T 相同。让我感到困惑的是decltype(x)int&amp;&amp;,但是,就像你的例子一样,foo(x) 选择了foo(int&amp;) 重载。 decltype(x) 也不一定是x 的类型吗?
    • decltype(x) 是“x 的声明类型”,即int&amp;&amp;。在没有forward 或没有任何转换的情况下使用x 会导致lvalue 表达式,调用foo(int&amp;)
    • @SU3:更新了我的答案以显示 decltype(x) 演员表示例。
    • 还添加了内联 cmets - 如果您仍然不明白,请告诉我
    • @SU3 参数总是被认为是左值(它们被命名并且它们有一个地址)。否则请考虑当您调用f(42) 时会发生f(x){ x++;} 之类的情况:您将无法在内部修改x(不能分配给左值,因此x++ 不会编译)。这是我们大多数人在编程语言中不想要的。其次,正如@Bo Persson 所说,你不想要“意外”动作,那些非常危险......想象一下突然在不同的房子里醒来,你不会想要这个,对吗? (除非...)
    【解决方案4】:

    x 是 r-value 与 x 具有 r-value-reference 类型不同。

    R-value 是 expression 的属性,而 r-value-reference 是其 type 的属性。

    如果您实际上尝试将作为 r 值引用的变量传递给函数,则会将其视为左值。 decltype 误导您。 Try it and see:

    #include <iostream>
    #include <typeinfo>
    using namespace std;
    
    template<class T> struct wrap { };
    
    template<class T>
    void bar(T &&value) { std::cout << " vs. " << typeid(wrap<T>).name() << std::endl; }
    
    template<class T>
    void foo(T &&value) { std::cout << typeid(wrap<T>).name(); return bar(value); }
    
    int main()
    {
        int i = 1;
        foo(static_cast<int &>(i));
        foo(static_cast<int const &>(i));
        foo(static_cast<int &&>(i));
        foo(static_cast<int const &&>(i));
    }
    

    输出:

    4wrapIRiE   vs. 4wrapIRiE
    4wrapIRKiE vs. 4wrapIRKiE
    4wrapIiE      vs. 4wrapIRiE(这些应该匹配!)
    4wrapIKiE   vs. 4wrapIRKiE (这些应该匹配!)

    【讨论】:

    • 不相关:您可以使用 ./a.out | c++filt 过滤掉(如果使用 UNIX/Linux,就像您使用的那样)以获得漂亮的(去错位的)类型名输出。
    猜你喜欢
    • 1970-01-01
    • 2012-10-08
    • 1970-01-01
    • 2011-07-19
    • 2018-07-30
    • 2016-11-15
    • 1970-01-01
    • 2015-05-10
    • 1970-01-01
    相关资源
    最近更新 更多