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

时间:2015-07-11 17:23:35

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

如果查看get std::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,我的程序只打印&#34; Moved&#34;无论我是按值返还还是右值参考,都会发生一次。

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

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

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

1 个答案:

答案 0 :(得分:4)

如何使用这个来创建悬空参考的示例非常有趣,但从示例中学习正确的教训非常重要。

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

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

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

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

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<int>())和move-construct到c。你可以在那里对concat进行100次嵌套调用,而不需要任何额外的构造。

因此,&&返回可能会更快。因为,是的,移动速度比复制速度快,但如果可以避免复制,它的速度会更快。

总之,它已经为速度做了。考虑在元组内的元组内使用嵌套的get系列。此外,它允许它使用既没有复制也没有移动构造函数的类型。

这并未引入任何有关终身的新风险。 vector<int>().front()&#34;问题&#34;不是新的。