0

Suppose we have a function:

Foo bar(Foo&& foo)
{
    // assume `Foo` is move constructible
    return std::move(foo);
}

In this example:

// (1)
Foo foo = bar(Foo{});

And this one:

// (2)
Foo foo;

// do something with `foo` so that compiler can't optimize it away
...

Foo foo2 = bar(std::move(foo));

I know in (2) we almost definitely can't avoid a default construction (foo) and a move construction (return value of bar), but the third move construction (from the return value of bar into foo2) can be elided by the compiler.

But how about (1)? There are semantically two move constructions (one from the temporary argument into bar's return value, the other from the return value into foo), where the second one can almost always be elided. But how about the first one? Is it ever possible that some aggressive optimization can make only one construction happen in example (1), so that it is effectively the same as:

Foo foo;

Is that possible?

Zizheng Tai
  • 6,170
  • 28
  • 79
  • It will be better if you can print something inside all the constructors (like "default c'tor", "move c'tor") in order to understand what are you expecting. And then share the output. – sameerkn Jul 14 '16 at 06:45
  • @sameerkn See deepmax's answer below. – Zizheng Tai Jul 14 '16 at 06:55
  • `*bar() is receiving an argument*`, therefore the object received in argument has to be constructed for it to be valid. `*bar() is returning an object*` which may or may not be collected, therefore either `move` or `copy` constructor will be involved. Hence 2 construction will always be there. Elision also involves a construction. – sameerkn Jul 14 '16 at 07:25
  • @sameerkn this is true but function inlining can potentially be optimized to just one object, though for GCC (for example) you'd need `-O3` level optimization for it to inline functions. On `-O2` and lower I completely agree. – Xeren Narcy Jul 14 '16 at 07:28

2 Answers2

0

It can but it's not exactly safe to use outside of this intent, please take care using this.

You can use rvalues to completely elide construction, but this means your intermediate functions (ie bar) have to pass and accept reference parameters. It's not hard to stick to just rvalue references so something like this is possible:

class A { /* ... */ };
A && bar (A && a) {return std::move(a);}
A a1 = A();
A a2 = bar( A() );
A a3 = bar( bar( A() ) );

Rule of thumb: if you're passing by value, you can only remove spurious intermediate object instances via optimization, because the compiler has to inspect usage of the instance and re-arrange. However with references we outright declare we require an object that's already available, and rvalues extend this to temporary objects.

The "gotcha" with reference passing like this is if you pass a reference to an object who's scope expired you're in trouble. (eg, returning an rvalue reference to a local variable, no better than a regular reference but that case is likely to be flagged by the compiler whereas rvalues may not).

A && bad_valid_return() {
    A temp;
    return std::move(temp);
}
A a4 = bad_valid_return(); // not ok, object is destroyed once we return!

A good_valid_return() {
    A temp;
    return std::move(temp);
    // better but we can only remove the result's construction via optimize
}

For what it's worth you can also do this:

A && a5 = good_valid_return();

a5 is in essence a regular variable (it's type will be A&, virtually identical to A a5 when used). It will hold the actual temporary that is returned and as consequence the temporary is constructed in place. This can even get around private object members - a5 assignment would work even if operator= and the constructor were private (and friended to good_valid_return).


Regarding bar

This will almost never be allowed, in the general case, to completely avoid move operators. Consider every "by value" value passing as a construction barrier - values crossing that point must result with a new container except where it's "obvious" that only one object will ever be needed.

These "obvious" cases are the return-value-optimization cases. As separate code units, functions cannot know how their values will be used, so functions trading in "by value" passing must assume a new object will be needed.

The exception to this is inlining - the function code is optimized where it is referred to, essentially removing the function call altogether - I expect that to be the only time your (1) case will be optimized away from any extra objects / moves / copies without using rvalue references to directly tell the compiler that that's what you intend to happen.

It does look like that's what can happen in (1) but you would need at least -O3 optimization with GCC for example, for that to happen. (maybe O2 but I wouldn't think so)

Xeren Narcy
  • 875
  • 5
  • 15
  • FWIW I have two extensive answers on [this question](http://stackoverflow.com/questions/37871609/returning-an-argument-passed-by-rvalue-reference/38040678) that mention explicit elides. – Xeren Narcy Jul 14 '16 at 07:10
0

If I remember correctly, RVO doesn't apply here for two reasons:

  1. The returned type (R-value-reference to Foo) isn't the same as the function's return type
  2. The returned value isn't a local variable or temporary

If however, your copy/move constructor doesn't have any side effects (doesn't work with deepmax's Foo class) and bar is simple enough, I'm pretty sure that a modern compiler is able to inline bar and optimize away everything except the default-constructor under the as-if rule.

MikeMB
  • 20,029
  • 9
  • 57
  • 102