Vitality
Vitality

Reputation: 21475

Rvalue references under the hood

Consider the foo function

void foo(X x);

with X a matrix cass, and the function

X foobar();

Suppose that I run

foo(foobar());

What happens with temporary objects in this case step by step? My understanding is that

  1. foobar returns a temporary object, say Xtemp1;
  2. foo copies Xtemp1 to a temporary object of its own, say Xtemp2, and then destroyes Xtemp1;
  3. foo performs calculations on Xtemp2.

On the other side, if I overload foo as

void foo(X& x);
void foo(X&& x);

then the picture will be different and, in particular,

  1. foobar returns the temporary Xtemp1;
  2. foo does not create a new temporary, but acts directly on Xtemp1 through its reference.

Is this picture correct or, if not, could someone point out and fix my mistakes? Thank you very much.

Upvotes: 2

Views: 364

Answers (2)

Nicol Bolas
Nicol Bolas

Reputation: 473437

foo(foobar());
  1. foobar's return value is a value, so it's a temporary value.
  2. 1This function will move/copy the return value into local storage for a temporary.
  3. 1This function will move/copy the value in the local storage into the parameter for the call to foo.
  4. The call to foo executes.
  5. 2As foo returns, it's parameter is destructed.
  6. 2When foo returns, the value in local storage is destructed.
  7. foobar's return value is destructed.

1 These moves/copies may be elided by the compiler. This means that these copy/moves don't have to happen (and any compiler worth using will elide them).

2 If the moves/copies above are elided, then the destruction of their respective variables are naturally unnecessary. Since they don't exist.

On the other side, if I overload foo as

  1. foobar's return value is a value, so it's a temporary value.
  2. 1This function will move/copy the return value into local storage for a temporary.
  3. Overload resolution selects foo(X &&) to call.
  4. An rvalue-reference parameter variable is created and initialized with the local storage value.
  5. The call to foo executes.
  6. As foo returns, it's reference parameter is destructed (not the value it references).
  7. 2When foo returns, the value in local storage is destructed.
  8. foobar's return value is destructed.

Note the key differences here. Step 4 and 6 cannot be elided. Therefore, if X is a small type like int, then the function will have no choice but to create a fairly worthless reference to an integer. References are internally implemented as pointers, so it's not really possible for the compiler to just optimize that away as a register. The local storage must therefore be on the stack rather than a register.

So you will be guaranteed of fewer move/copies. But again, any decent compiler will elide them. So the question is, will X generally be too large to fit into a register?

Upvotes: 2

Your understanding is almost correct. The only difference is that in step 2., the temporary Xtemp1 is not copied to an arbitrary-named temporary Xtemp2, but to the space of the formal parameter x (from declaration foo(X x)).

Also, it's possible for copy-elision to kick in, which could mean that the return value of foobar() is constructed directly in the space of foo's formal parameter x, thus no copying would occur. This is allowed by the standard, but not guaranteed.

Your take on the r-value reference case is correct.

Upvotes: 1

Related Questions