为什么获取std :: tuple的帮助器返回右值引用而不是值 [英] Why does get helper of std::tuple return rvalue reference instead of value

查看:131
本文介绍了为什么获取std :: tuple的帮助器返回右值引用而不是值的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

 模板< 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:<<这个<< std :: endl;
}

〜Hello(){
std :: cerr<< Destructed at:<<这个<< 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;
}

运行时,此程序打印:

 构造于:0x7ffc0e12cdc0 
毁坏在:0x7ffc0e12cdc0
0

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

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

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

  Hello x(foo()。get 

看起来似乎没有任何额外的开销,无论你返回值还是rvalue引用。我测试了像这样的代码,它似乎将相当一致只执行单一的移动构造。例如。如果我添加了一个成员:

 你好(你好&){std :: cerr< 移动< std :: endl; } 

我构造x如上,我的程序只打印一次



有没有很好的理由,或者这是一个监督?



注意:这里有一个很好的相关问题:返回值或右值引用? 。似乎说,在这种情况下,价值回报通常是优选的,但是它出现在STL的事实使我很好奇STL是否忽略了这个推理,或者如果他们有自己的特殊原因,可能不适用一般。



编辑:有人建议此问题与是否有任何RValue引用(&&)的返回有用的情况?不是这种情况;这个答案建议通过右值引用返回作为一种方式来复制数据成员。正如我上面详细讨论的,无论你返回值还是右值引用,只要你先调用 move ,复制就会被省略。

您可以使用这个例子来创建悬挂引用非常有趣,但是从示例中学习正确的教训很重要。



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

  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&二_);
l.insert(l.end(),r.cbegin(),r.cend());
return l;
}

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

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

这种优化使构造数最小化

 矢量< 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 )和move-construct into c 。你可以有100个嵌套调用 concat ,没有任何额外的结构。



返回&&& 可以更快,因为,是的,移动比副本更快,但如果你可以避免这两者,它会更快。



总之,这是为了速度。考虑使用嵌套系列的 get 在一个tuple-within-a-tuple-within-a



这不会引入任何有关生命周期的新风险,<$ p c $ c> vector< int>()。front()问题不是一个新的。


If you look at get, the helper function for std::tuple, you will notice the following overload:

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

In other words, it returns an rvalue reference when the input tuple is an rvalue reference itself. Why not return by value, calling move in the function body? My argument is as follows: the return of get will either be bound to a reference, or to a value (it could be bound to nothing I suppose, but this shouldn't be a common use case). If it's bound to a value, then a move construction will anyway occur. So you lose nothing by returning by value. If you bind to a reference, then returning an rvalue reference can actually be unsafe. To show an example:

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

When run, this program prints:

Constructed at : 0x7ffc0e12cdc0
Destructed at : 0x7ffc0e12cdc0
0

In other words, x is immediately a dangling reference. Whereas if you just wrote foo like this:

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

This problem would not occur. Furthermore, if you then use foo like this:

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

It doesn't seem like there is any extra overhead whether you return by value, or rvalue reference. I've tested code like this, and it seems like it will quite consistently only perform a single move construction. E.g. if I add a member:

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

And I construct x as above, my program only prints "Moved" once regardless of whether I return by value or rvalue reference.

Is there a good reason I'm missing, or is this an oversight?

Note: there is a good related question here: Return value or rvalue reference?. It seems to say that value return is generally preferable in this situation, but the fact that it shows up in the STL makes me curious whether the STL has ignored this reasoning, or if they have special reasons of their own that may not be as applicable generally.

Edit: Someone has suggested this question is a duplicate of Is there any case where a return of a RValue Reference (&&) is useful?. This is not the case; this answer suggests return by rvalue reference as a way to elide copying of data members. As I discuss in detail above, copying will be elided whether you return by value or rvalue reference provided you call move first.

解决方案

Your example of how this can be used to create a dangling reference is very interesting, but it's important to learn the correct lesson from the example.

Consider a much simpler example, that doesn't have any && anywhere:

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

.front() returns an &-reference to the first element of the new constructed vector. The vector is immediately destroyed of course and you are left with a dangling reference.

The lesson to be learned is that using a const-reference does not, in general, extend the lifetime. It extends the lifetime of non-references. If the right hand side of = is a reference, then you have to take responsibility for lifetimes yourself.

This has always been the case, so it wouldn't make sense for tuple::get to do anything different. tuple::get is permitted to return a reference, just as vector::front has always been.

You talk about move and copy constructors and about speed. The fastest solution is to use no constructors whatsoever. Imagine a function to concatenate two vectors:

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

This would allow an optimized extra overload:

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

This optimization keeps the number of constructions to a minimum

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

The final line, with four calls to concat, will only have two constructions: The starting value (vector<int>()) and the move-construct into c. You could have 100 nested calls to concat there, without any extra constructions.

So, returning by && can be faster. Because, yes, moves are faster than copies, but it's even faster still if you can avoid both.

In summary, it's done for speed. Consider using a nested series of get on a tuple-within-a-tuple-within-a-tuple. Also, it allows it to work with types that have neither copy nor move constructors.

And this doesn't introduce any new risks regarding lifetime. The vector<int>().front() "problem" is not a new one.

这篇关于为什么获取std :: tuple的帮助器返回右值引用而不是值的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆