6

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.

Community
  • 1
  • 1
Nir Friedman
  • 17,108
  • 2
  • 44
  • 72
  • This is not remotely a duplicate. My answer explicitly and in detail discusses how returning by value and calling std::move beforehand achieves the same effect of moving from a data member. My question is related to the differences in the two approaches. The question that I cited myself is more related to my question than the one you cited. Please read more carefully before rushing to label something a duplicate. – Nir Friedman Jul 11 '15 at 17:50
  • if you return by value - you create a copy. if you move it, you move a copy. if you return a r-value-reference you can either move it , then the original object is moved, or not deal with it with move sematics, then it will behave like regular reference. the point is that if you return a r-value-reference you don't force the copying. – David Haim Jul 11 '15 at 17:53
  • Why require move constructability? – Columbo Jul 11 '15 at 17:57
  • @DavidHaim You don't create a copy, as you are free to verify for yourself. When you return by value but cast the return to an rvalue, it move constructs the return. – Nir Friedman Jul 11 '15 at 17:58
  • @Columbo This is an interesting case. But if the contained types are not movable, the tuple will not be, and its fairly unlikely you would even get an rvalue reference to a tuple in the first place. The question in my mind then is does this outweigh the danger surrounding dangling references? – Nir Friedman Jul 11 '15 at 18:03
  • @NirFriedman this is wrong. look here : http://coliru.stacked-crooked.com/a/b5421a862dcb7af3 there is still copying involved. checked also on VC++ with the same results – David Haim Jul 11 '15 at 18:03
  • @DavidHaim Your example doesn't prove anything, you don't call std::move before you return. Look at this: http://coliru.stacked-crooked.com/a/d48d6a8db4699180. You also are using a function instead of a method returning a member, which is different. Not sure why you wouldn't just use the code that I posted if you wanted to check if a copy happens. – Nir Friedman Jul 11 '15 at 18:07
  • I see now. but look at this : http://coliru.stacked-crooked.com/a/e4ae036420c0d18c your's invoked 2 move construction. my example now only invoked it once without calling std::move. I guess this is the reason - it saves one construction (which can be expensive) plus it doesn't require the developer to explecitly invoke std::move, allowing the compiler to choose the best of the options. – David Haim Jul 11 '15 at 18:13
  • http://coliru.stacked-crooked.com/a/96ab117d372999d3. The extra move was called by your own redundant std::move in the wrong place. All I did was take your last example and removed &&, and the behavior in this situation (assigning to a value) is identical, which is what I've said all along... – Nir Friedman Jul 11 '15 at 18:19

1 Answers1

4

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.

Aaron McDaid
  • 26,501
  • 9
  • 66
  • 88
  • 2
    I need to spend a bit more time with your answer to fully absorb it. But note that ironically, your example with vector.front would not be generous if there was an overload T front() &&. – Nir Friedman Jul 11 '15 at 19:01
  • 2
    I thought of mentioning that. Yes, the committee should put `&` and `&&` throughout the standard, just as your example in your comment. But I guess that would break compatibility. Also, perhaps all methods should be `&` by default, but that too would be a pretty breaking dramatic change to the language. – Aaron McDaid Jul 11 '15 at 19:04
  • To generalize, if a function returns one of the arguments, or a subobject of an argument, and that argument is `&&`, then I see no reason not to always return by `&&`. Am I going too far here? – Aaron McDaid Jul 11 '15 at 19:18
  • Well, the safety argument still exists I think, if the argument into the function is a temporary and you return && and someone binds a reference to that, it will still dangle. But your performance argument is persuasive. Here is code I used to verify this: http://coliru.stacked-crooked.com/a/fac7f0d7ca7f8d67. Three extra move constructors called. Also note a minor typo in your answer, you can't overload by value and rvalue, you'll need to change the first concat to take lvalue references for both arguments. – Nir Friedman Jul 11 '15 at 19:29
  • 1
    Thanks for pointing out the problem of ambuigity of value and rvalue. – Aaron McDaid Jul 11 '15 at 19:47
  • 2
    I must admit I'm starting to have some doubts about my answer, after reading [this answer](http://stackoverflow.com/a/6030496/146041)! E.g. `for( x : ) { ... }` would fail if `` was a function returning an rv-ref. So perhaps it should be used only very rarely (i.e. not in the standard library) – Aaron McDaid Jul 11 '15 at 19:57
  • 1
    Great example! That's very interesting, I'll dig deeper into it. I appreciate your providing a good answer, this question has been a bit of a battle :-) – Nir Friedman Jul 11 '15 at 20:21