为什么获取 std::tuple return 右值引用而不是值的助手

Why does get helper of std::tuple return rvalue reference instead of value

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

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

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

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());

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

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

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

我失踪是有充分理由的,还是疏忽?

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

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

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

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

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

.front() returns 一个 & 对新构造向量的第一个元素的引用。 vector 当然会立即被销毁,你会留下一个悬空的引用。

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

情况一直如此,因此 tuple::get 做任何不同的事情都没有意义。 tuple::get 允许 return 引用,就像 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>()) 和进入 c 的移动结构。您可以在那里对 concat 进行 100 次嵌套调用,而无需任何额外的构造。

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

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

而且这不会引入任何关于使用寿命的新风险。 vector<int>().front() "problem" 不是新的。