为什么得到std :: tuple的帮助器返回rvalue引用而不是value

Nir*_*man 5 c++ stl move-semantics c++11

如果你看一下get辅助函数std::tuple,你会注意到以下的重载:

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

换句话说,当输入元组本身是右值引用时,它返回一个右值引用.为什么不按值返回,调用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;
}
Run Code Online (Sandbox Code Playgroud)

运行时,该程序打印:

Constructed at : 0x7ffc0e12cdc0
Destructed at : 0x7ffc0e12cdc0
0
Run Code Online (Sandbox Code Playgroud)

换句话说,x立即成为悬空参考.如果你只是写这样的foo:

struct foo {
  Hello m_hello;
  Hello get() && { return std::move(m_hello); }
};
Run Code Online (Sandbox Code Playgroud)

这个问题不会发生.此外,如果你然后像这样使用foo:

Hello x(foo().get());
Run Code Online (Sandbox Code Playgroud)

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

  Hello(Hello && ) { std::cerr << "Moved" << std::endl; }
Run Code Online (Sandbox Code Playgroud)

我按上面的方式构造x,我的程序只打印"Moved"一次,无论我是按值还是右值引用返回.

我错过了一个很好的理由,还是这是一个疏忽?

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

编辑:有人建议这个问题是重复的是否有任何返回RValue参考(&&)有用的情况?.不是这种情况; 这个答案建议通过右值引用返回作为忽略数据成员复制的方法.正如我在上面详细讨论的那样,如果您move先调用,无论您是按值返还还是右值引用,都将省略复制.

Aar*_*aid 4

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

考虑一个更简单的例子,它没有任何&&地方:

const int &x = vector<int>(1) .front();
Run Code Online (Sandbox Code Playgroud)

.front()返回&对新构造的向量的第一个元素的引用。当然,向量会立即被破坏,并且留下一个悬空的引用。

要吸取的教训是,使用const- 引用通常不会延长生命周期。它延长了非引用的生命周期。如果右侧=是参考,那么你就得自己承担一生的责任。

情况一直如此,所以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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

   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);
Run Code Online (Sandbox Code Playgroud)

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

所以,返回&&可以更快。因为,是的,移动比复制更快,但如果你能避免两者,它甚至会更快。

总而言之,这样做是为了速度。get考虑在元组内的元组内使用嵌套系列。此外,它还允许它与既没有复制也没有移动构造函数的类型一起使用。

这不会带来任何关于生命周期的新风险。这个vector<int>().front()“问题”并不是一个新问题。

  • 我需要花更多的时间来理解你的答案才能完全吸收它。但请注意,具有讽刺意味的是,如果存在重载 T front() &amp;&amp;,则您使用 vector.front 的示例将不会很慷慨。 (2认同)
  • 我想到了提到这一点。是的,委员会应该在整个标准中添加“&amp;”和“&amp;&amp;”,就像您在评论中的示例一样。但我想这会破坏兼容性。另外,也许所有方法都应该默认为“&amp;”,但这也将是对语言的一个相当重大的突破性改变。 (2认同)
  • 我必须承认,在阅读[这个答案](http://stackoverflow.com/a/6030496/146041)后,我开始对我的答案产生一些疑问!例如,如果“&lt;expr&gt;”是返回 rv-ref 的函数,“for( x : &lt;expr&gt;) { ... }”将会失败。所以也许它应该很少使用(即不在标准库中) (2认同)