【问题标题】:Why does get helper of std::tuple return rvalue reference instead of value为什么 std::tuple 的助手返回右值引用而不是值
【发布时间】:2015-09-30 07:52:16
【问题描述】:

如果您查看getstd::tuple 的辅助函数,您会注意到以下重载:

template< std::size_t I, class... Types >
constexpr std::tuple_element_t<I, tuple<Types...> >&&
get( tuple<Types...>&& t );

换句话说,当输入元组本身是一个右值引用时,它返回一个右值引用。为什么不按值返回,在函数体中调用move?我的论点如下: get 的返回将被绑定到一个引用或一个值(我想它可能没有绑定,但这不应该是一个常见的用例)。如果它绑定到一个值,那么无论如何都会发生移动构造。因此,按值返回不会有任何损失。如果绑定到引用,则返回右值引用实际上可能是不安全的。举个例子:

struct Hello {
  Hello() {
    std::cerr << "Constructed at : " << this << std::endl;
  }

  ~Hello() {
    std::cerr << "Destructed at : " << this << std::endl;
  }

  double m_double;
};

struct foo {
  Hello m_hello;
  Hello && get() && { return std::move(m_hello); }
};

int main() {
  const Hello & x = foo().get();
  std::cerr << x.m_double;
}

运行时,该程序会打印:

Constructed at : 0x7ffc0e12cdc0
Destructed at : 0x7ffc0e12cdc0
0

换句话说,x 立即是一个悬空引用。而如果你只是这样写 foo:

struct foo {
  Hello m_hello;
  Hello get() && { return std::move(m_hello); }
};

不会发生此问题。此外,如果您像这样使用 foo:

Hello x(foo().get());

无论您是按值返回还是按右值引用返回,似乎都没有任何额外的开销。我已经测试过这样的代码,似乎它会始终只执行一次移动构造。例如。如果我添加一个成员:

  Hello(Hello && ) { std::cerr << "Moved" << std::endl; }

并且我按照上面的方法构造 x,我的程序只打印一次“已移动”,无论我是按值返回还是按右值引用返回。

我失踪是否有充分的理由,或者这是一个疏忽?

注意:这里有一个很好的相关问题:Return value or rvalue reference?。似乎说在这种情况下价值回报通常更可取,但它出现在 STL 中的事实让我很好奇 STL 是否忽略了这个推理,或者他们是否有自己的特殊原因可能不适用一般。

编辑:有人建议此问题与Is there any case where a return of a RValue Reference (&&) is useful? 重复。不是这种情况;这个答案建议通过右值引用返回作为省略数据成员复制的一种方式。正如我在上面详细讨论的那样,如果您先调用move,则无论您是按值返回还是按右值引用返回,复制都将被省略。

【问题讨论】:

  • 这不是远程复制。我的回答明确而详细地讨论了按值返回和预先调用 std::move 如何达到从数据成员移动的相同效果。我的问题与两种方法的差异有关。我自己引用的问题与我的问题比您引用的问题更相关。在匆忙标记重复内容之前,请仔细阅读。
  • 如果您按值返回 - 您将创建一个副本。如果你移动它,你移动一个副本。如果你返回一个右值引用,你可以移动它,然后移动原始对象,或者不使用移动语义处理它,那么它的行为就像常规引用一样。关键是如果你返回一个右值引用,你不会强制复制。
  • 为什么需要移动可施工性?
  • @DavidHaim 您无需创建副本,因为您可以自行验证。当您按值返回但将返回转换为右值时,它会移动构造返回。
  • @Columbo 这是一个有趣的案例。但是,如果包含的类型不可移动,则元组将不会移动,而且您甚至不太可能首先获得对元组的右值引用。那么我心中的问题是,这是否超过了悬空引用的危险?

标签: c++ c++11 stl move-semantics


【解决方案1】:

关于如何使用它来创建悬空引用的示例非常有趣,但从示例中吸取正确的教训很重要。

考虑一个更简单的例子,它在任何地方都没有任何&amp;&amp;

const int &x = vector<int>(1) .front();

.front() 返回一个&amp;-reference,指向新构造的向量的第一个元素。当然,该向量会立即被销毁,并且您会留下一个悬空的引用。

要吸取的教训是,使用const-reference 通常不会延长生命周期。它延长了非引用的生命周期。如果= 的右手边是一个引用,那么你必须自己对生命周期负责。

情况一直如此,所以tuple::get 做任何不同的事情是没有意义的。 tuple::get 被允许返回引用,就像 vector::front 一直以来一样。

您谈论移动和复制构造函数以及速度。最快的解决方案是不使用任何构造函数。想象一个连接两个向量的函数:

vector<int> concat(const vector<int> &l_, const vector<int> &r) {
    vector<int> l(l_);
    l.insert(l.end(), r.cbegin(), r.cend());
    return l;
}

这将允许优化的额外重载:

vector<int>&& concat(vector<int>&& l, const vector<int> &r) {
    l.insert(l.end(), r.cbegin(), r.cend());
    return l;
}

这种优化将构造数量保持在最低限度

   vector<int> a{1,2,3};
   vector<int> b{3,4,5};
   vector<int> c = concat(
     concat(
       concat(
          concat(vector<int>(), a)
       , b)
      , a
   , b);

最后一行,对concat 进行了四次调用,只有两个构造:起​​始值 (vector&lt;int&gt;()) 和移动构造到 c。您可以在那里对 concat 进行 100 次嵌套调用,而无需任何额外的构造。

因此,通过&amp;&amp; 返回可以更快。因为,是的,移动比复制快,但如果你能同时避免两者,它会更快。

总之,这是为了速度。考虑在 tuple-within-a-tuple-within-a-tuple 上使用 get 的嵌套系列。此外,它允许它处理既没有复制也没有移动构造函数的类型。

这并没有引入任何关于生命周期的新风险。 vector&lt;int&gt;().front()“问题”并不是一个新问题。

【讨论】:

  • 我需要多花一点时间来回答你的问题才能完全吸收它。但请注意,具有讽刺意味的是,如果有一个重载 T front() &&.
  • 我想提一下。是的,委员会应该将&amp;&amp;&amp; 放在整个标准中,就像您在评论中的示例一样。但我想这会破坏兼容性。此外,默认情况下,也许所有方法都应该是 &amp;,但这也将是对语言的一次非常重大的重大改变。
  • 感谢指出value和rvalue不明确的问题。
  • 我必须承认,在阅读了this answer 之后,我开始对自己的答案产生怀疑!例如。如果 &lt;expr&gt; 是返回 rv-ref 的函数,for( x : &lt;expr&gt;) { ... } 将失败。所以也许它应该很少使用(即不在标准库中)
  • 很好的例子!这很有趣,我会更深入地研究它。感谢您提供一个很好的答案,这个问题有点激烈:-)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-08-28
  • 2015-12-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-08-31
相关资源
最近更新 更多