4

We have the following type X and function f:

struct X { ... };

X f() { ... };

Now consider three alternative definitions of another function g:

(1)

void g()
{
    X x = f();

    ...
}

(2)

void g()
{
    X& x = f();

    ...
}

(3)

void g()
{
    X&& x = f();

    ...
}

What is the difference in defined behavior (or potential behaviour) between the three different cases? (assume the placeholdered '...' code is identical in the three cases)

Update:

What if g returned an X: is the following legal and correct?

X g()
{
    X&& x = f();

    ...

    return move(x);
}

(Is the move necessary, does it do anything?)

Would you expect RVO to chain so the below would produce the same code?

X g()
{
    X x = f();

    ...

    return x;
}
Andrew Tomazos
  • 66,139
  • 40
  • 186
  • 319
  • checkout http://stackoverflow.com/questions/13618506/is-it-possible-to-stdmove-objects-out-of-functions-c11/13618587#13618587 – billz Jan 07 '13 at 00:15
  • 2
    Number 2 isn't valid C++. The difference between (1) and (3) is the type of the *variable* `x`, but the value of the *expression* `x` is the same in both. – Kerrek SB Jan 07 '13 at 00:16

2 Answers2

6

2 is illegal. The other two are essentially identical- x is a mutable lvalue of type X whose life ends when g() returns.

Of course, strictly, the first calls the move constructor (but is a prime candidate for some RVO/NRVO action) and the third does not, so if X is immovable (very odd...) the third case is legal but the first is not, and the first may be more expensive. However, the reality of compiler opts and immovable types is that this is almost entirely a technicality and I'd be surprised if you could actually demonstrate any such case.

Puppy
  • 144,682
  • 38
  • 256
  • 465
  • They don't, realistically. – Puppy Jan 07 '13 at 00:10
  • @AndrewTomazosFathomlingCorps You should use (1) whenever you can because it's simpler and doesn't depend on knowledge of C++ arcana. Use (3) only when you must. – bames53 Jan 07 '13 at 00:20
1

The expression f() returns by value, so it's a prvalue.

  1. this creates a new object of type X initialized with the expression f() which is a prvalue of type X. How the new X object will be constructed depends on whether it has a move constructor or not. If it has a move constructor (or a template constructor that accepts X rvalues) it will be called, otherwise if it has a copy constructor that will be called. In practice the compiler will almost certainly elide the constructor call, but the appropriate constructor must be accessible and not deleted.

  2. This doesn't even compile, you get a downvote for not trying it before posting!

  3. This create a new reference of type X&& and initializes it by binding it to the prvalue returned by f(). That prvalue will have its lifetime extended to the same lifetime as the reference x.

The difference in behaviour is probably nothing, assuming the move/copy is elided, but there is a difference in semantics between (1) and (3) because one does overload resolution for a constructor, which could fail, and the other always works.

What if g returned an X: is the following legal and correct?

It's legal. The move is unnecessary, there's a special rule that says when returning a local variable by value the constructor lookup is first done as though the variable was an rvalue so if X has a move constructor it will be used, whether or not you use move(x). RVO should "chain". If g returned X& you'd have a problem in both cases, because the object it would bind to would go out of scope at the end of g.

(N.B. It's good practice to always qualify std::move to prevent ADL. Similarly for std::forward. If you mean to call std::move or std::forward then be explicit, don't rely on there being no overloads in scope or visible, move and forward are not customization points like swap.)


Instead of learning C++ by asking questions on SO, why not write code to test what happens and prove it to yourself? With G++ you can use the -fno-elide-constructors flag to turn off constructor elision to see what would happen in the absence of elision, and when not using that flag (the default) you can easily test whether RVO "chains" for yourself.

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
  • +1 to the overall answer, however learning _C++_ by writing code and seeing if it works is not that a good way to learn with _undefined behavior_ all over the place – K-ballo Jan 07 '13 at 22:31
  • A decent compiler will warn about returning references to temporaries, for example, and would certainly have shown (2) isn't even valid! It's not good in general, but would answer almost every part of this SO question. – Jonathan Wakely Jan 07 '13 at 22:37
  • 1
    (2) doesn't even compile, experimentation will easily tell you that, so you clearly hadn't even attempted the most cursory experiments before asking. And the question isn't clearly about _un_defined behaviour, it's mostly about what happens with entirely defined behaviour – Jonathan Wakely Jan 07 '13 at 23:00
  • 1
    I disagree with "the move is unnecessary". He names an rvalue reference, not an object variable with automatic storage duration. – Johannes Schaub - litb Jan 08 '13 at 20:05
  • @JohannesSchaub-litb, good point, thanks! So that's a good argument for preferring (1) to (3), as it doesn't prevent elision – Jonathan Wakely Jan 08 '13 at 20:10