19

Consider the following code. What happens when doStuff() is called but the return value is not used? Is SomeClass still created? Of course the creation itself can have important side effects, but so can copy-constructors and they are still omitted in RVO / copy-elision.

SomeClass doStuff(){
    //...do stuff
    return SomeClass( /**/);
}

SomeClass some_object = doStuff();
doStuff(); //What happens here?

(Edit: tested this with GCC -O3. The object is constructed and then destructed right away)

AdyAdy
  • 988
  • 6
  • 19
  • 1
    An object returned by a function will remain valid until it goes out of scope. This means that the enclosing scope where the function is called from, then it goes out of scope any object destructor will be called. In your example you have not assigned anything to the second call, but the same applies. – SPlatten Feb 14 '18 at 08:28
  • 3
    @SPlatten That is not true. In the example the copy constructor is called for `some_class` with the exception of RVO. The returned object obeys the same rule, as in it is a temporary and will be destructed immediately after the statement – Passer By Feb 14 '18 at 08:33
  • 5
    I'd look at it this way : suppose that function is inside an already compiled third-party library. How would the compiler know in advance whether the returned object will be ignored once called in the client code ? Now, would it be desirable/sensible to have functions behave differently depending on whether the definition is accessible or not ? Well, that doesn't really answer your question, but I think that gives a big hint on what has been decided in the standard. – Caninonos Feb 14 '18 at 08:34
  • I would say it's somewhat like `{SomeClass s{};}` (the constructor and the destructor must be called (at lease as-if)) – apple apple Feb 14 '18 at 08:59
  • @PasserBy, the rules of scope and lifetime of an object apply to all objects copied or otherwise. If the object is not global and created in a scope then it will be destroyed (its destructor) called when it goes out of scope. – SPlatten Feb 14 '18 at 09:09

2 Answers2

22

I feel there's a misunderstanding when it comes to RVO and copy elision. It doesn't mean that a function's return value is not created. It's always created, that's not something an implementation can cop out of doing.

The only leeway, when it comes to eliding copies, despite side effects, is with cutting the middle man. When you initialize an object with the result of the call, then the standard allows plugging the target object in, for the function to initialize directly.

If you don't provide a target object (by using the result), then a temporary must be materialized, and destroyed, as part of the full expression that contains the function call.

So to play a bit with your example:

doStuff(); // An object is created and destroyed as part of temporary materialization
           // Depending on the compilers analysis under the as-if rule, there may be
           // further optimization which gets rid of it all. But there is an object there 
           // formally.

std::rand() && (doStuff(), std::rand());
// Depending on the result of std::rand(), this may or may not create an object.
// If the left sub-expression evaluates to a falsy value, no result object is materialized.
// Otherwise, one is materialized before the second call to std::rand() and 
// destroyed after it.
StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • So, it is not possible for the compiler to optimize out such call? – Yola Feb 14 '18 at 08:39
  • 9
    @Yola - If no observable behavior is affected, it certainly may. Depends on the objects being created. But that's not about copy elision, it's about optimizing under the as-if rule. From the view point of the abstract C++ machine, there's an object there. – StoryTeller - Unslander Monica Feb 14 '18 at 08:41
  • 2
    You say "the standard allows plugging the target object" (emphasis on "allows"). Is it really "allowed" or rather "required" ? or maybe allowed/required depending on the case ? – Caninonos Feb 14 '18 at 08:42
  • 4
    @Caninonos - It's a very common optimization, one that a compiler better implement when it can if it wants to be relevant. [But the standard only says an implementation is *allowed*](https://timsong-cpp.github.io/cppwp/n4659/class.copy.elision#1), not obligated. It's also worth noting, the RVO is a different form of optimization, not directly related of guaranteed copy elision as C++17 mandates. – StoryTeller - Unslander Monica Feb 14 '18 at 08:44
  • @Caninonos - Well, actually, to refine my previous comment. It's only obligated to do it in a `constexpr` function. – StoryTeller - Unslander Monica Feb 14 '18 at 08:45
  • What does "plugging" as in "plugging the target object mean", *exactly*? – Dai Feb 14 '18 at 10:49
  • @Dai - It's a slight(ly heavy) paraphrasing on my part, for the standartese: [*"In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object"*](https://timsong-cpp.github.io/cppwp/n4659/class.copy.elision#1) – StoryTeller - Unslander Monica Feb 14 '18 at 10:51
  • @storyTeller: How is that possible if the returned object exists on the end of the stack? When the stack frame is popped the object will need to be relocated - and any references or pointers to it become invalidated. – Dai Feb 14 '18 at 11:08
  • @Dai - It's ABI dependent. But normally it's accomplished by passing the address of the target storage into the function. Then the construction just uses the storage there directly for construction. The standards wording just makes the C++ abstract machine play well with this. – StoryTeller - Unslander Monica Feb 14 '18 at 11:13
  • 4
    @Dai: Remember, C++ source code _describes a program_. It is not a sequence of instructions to be executed on a computer. That's what the compiled assembly is for. People sometimes refer to this as "optimisations", but it's actually just the fundamental nature of what the language _is_. It is true that your compiler can produce more or less efficient (and obfuscated) code depending on an "optimisation level" setting, but that has nothing to do with C++ being an _abstract representation_ of what you want your computer to do. – Lightness Races in Orbit Feb 14 '18 at 11:50
  • @LightnessRacesinOrbit - Ooo I like that description! Mind if I nick it for future use? – StoryTeller - Unslander Monica Feb 14 '18 at 11:52
  • 2
    @StoryTeller: No problem. I take PayPal ;) – Lightness Races in Orbit Feb 14 '18 at 11:52
  • I think you should incorporate the comment caveat about observable behavior and as-if rule in your answer. The question concerns a construct which has bitten many a benchmark programmer when the compiler detects there is no visible effect of the code. (It's basically applied Keynsianism: hire unemployed workers to dig holes and fill them up again so that there is some occupation. If the observable effect is zero you may as well skip the work and take a nap.) – Peter - Reinstate Monica Feb 14 '18 at 13:58
  • @PeterA.Schneider - I already had it in the comment next to the first statement. Added your point about getting rid of it all – StoryTeller - Unslander Monica Feb 14 '18 at 14:01
  • 1
    BTW I like "temporary materialisation" :) – Lightness Races in Orbit Feb 14 '18 at 19:35
  • @StoryTeller-UnslanderMonica I used to think about temporary materialization as just another step when "dissecting an expression". For instance, for `const int& x = 3;` I used to rationalize like "We're binding a reference to a prvalue so temporary materialization kicks in which converts prvalue to xvalue denoting the temporary object". However, I cannot apply the same way of reasoning for functions since functions are "more complex" prvalues than literals. Where would one even start talking about temp. materialization; before function evaluation or after the function returned an object? – domdrag Feb 26 '23 at 21:31
  • 1
    @domdrag - The temporary can't materialize before the function returns (even for simple reasons, like exiting via `throw` and thus not returning at all). It's also not a separate thing to returning an object, it's part of it. The temporary materialization conversion is basically standard speak for "create an object here". – StoryTeller - Unslander Monica Feb 26 '23 at 23:34
  • @StoryTeller-UnslanderMonica That explains it well; just a follow-up question in case we got NRVO. In the case of NRVO, I would presume that temporary materialization (as an event) occurs at the moment of the creation of the object which will be returned, so not at the point of returning. I'd say that at the point of the creation of that object, the function-call expression converts from prvalue to xvalue denoting that object. Am I correct? – domdrag Mar 10 '23 at 20:07
  • 1
    @domdrag - I'm of the opinion that it's a bit overthinking it, but yes that sounds right. In NRVO the result object is created earlier (though because the function hasn't really stopped executing yet, that is not observable in the caller). – StoryTeller - Unslander Monica Mar 11 '23 at 10:42
6

A compiler may elide an unnecessary copy in certain cases, even if it has side effects, yes.

A compiler may not elide an object's entire existence, if it has side effects.

If it doesn't have side effects then no result is observable so whether the existence happened or not is effectively a non-question.

tl;dr: the standard lists very specific elision opportunities, and this is not one of them.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055