25

It's stated in [C++11: 12.8/31] :

This elision of copy/move operations, called copy elision, is permitted [...] :

— in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value

This implies

#include <iostream>

using namespace std;

struct X
{
    X() { }
    X(const X& other) { cout << "X(const X& other)" << endl; }
};

X no_rvo(X x) {
    cout << "no_rvo" << endl;
    return x;
}

int main() {
    X x_orig;
    X x_copy = no_rvo(x_orig);

    return 0;
}

will print

X(const X& other)
no_rvo
X(const X& other)

Why is the second copy constructor required? Can't a compiler simply extend the lifetime of x?

Community
  • 1
  • 1
Valentin Milea
  • 3,186
  • 3
  • 28
  • 29
  • There are actually three objects which (may) get copy constructed in that example (the argument to `no_rvo`, the return value of `no_rvo` and `x_copy`). The construction of `x_copy` can be elided (by constructing the return value of `no_rvo` directly in `x_copy`). – Mankarse Feb 25 '12 at 14:37
  • @Mankarse: I think that's exactly what OP is asking - Why the construction of `x_copy` is not elided? – jweyrich Feb 25 '12 at 14:40
  • @jweyrich: No - the question is about why the construction of the return value is not elided. My point is just that it is inaccurate to say that the code _will_ print `X(const X& other) no_rvo X(const X& other)`, because the code could also print `X(const X& other) no_rvo X(const X& other) X(const X& other)` if the construction of `x_copy` is not elided. – Mankarse Feb 25 '12 at 14:42
  • @Mankarse: Ops, exactly. I see your point now. It __s/will/may/__ print. – jweyrich Feb 25 '12 at 14:50

2 Answers2

13

Imagine no_rvo is defined in a different file than main so that when compiling main the compiler will only see the declaration

X no_rvo(X x);

and will have no idea whether the object of type X returned has any relation to the argument. From what it knows at that point, the implementation of no_rvo could as well be

X no_rvo(X x) { X other; return other; }

So when it e.g. compiles the line

X const& x = no_rvo(X());

it will do the following, when maximally optimizing.

  • Generate the temporary X to be passed to no_rvo as argument
  • call no_rvo, and bind its return value to x
  • destruct the temporary object it passed to no_rvo.

Now if the return value from no_rvo would be the same object as the object passed to it, then destruction of the temporary object would mean destruction of the returned object. But that would be wrong because the returned object is bound to a reference, therefore extending its lifetime beyond that statement. However simply not destructing the argument is also no solution because that would be wrong if the definition of no_rvo is the alternative implementation I've shown above. So if the function is allowed to reuse an argument as return value, there can arise situations where the compiler could not determine the correct behaviour.

Note that with common implementations, the compiler would not be able to optimize that away anyways, therefore it is not such a big loss that it is not formally allowed. Also note that the compiler is allowed to optimize the copy away anyway if it can prove that this doesn't lead to a change in observable behaviour (the so-called as-if rule).

celtschk
  • 19,311
  • 3
  • 39
  • 64
  • So basically it would be incompatible with the (when taken together) elision of copy constructions of arguments and separate compilation (because if elision of an argument's copy constructor is performed it must be performed by the caller (because at (separate) compilation time the callee _cannot_ know whether its argument is being constructed from a temporary or not)). – Mankarse Feb 25 '12 at 15:24
  • 1
    Thanks for the explanation! Would a calling convention in which the _function_ takes responsibility for destructing its pass-by-value arguments work? Then the caller no longer cares how no_rvo is defined. – Valentin Milea Feb 25 '12 at 19:43
  • My interpretation: The call site needs to know how to pass arguments to the called function without knowing the *implementation* of the called function (e.g., if the called function is defined in a separate translation unit) to produce the calling procedures. But RVO with a parameter would be an implementation-dependent optimization, and it would affect the interface by which the calling code has to pass arguments. Hence, you can't do RVO with parameters, because calling code is supposed to be unaware of the called function's implementation. – Alexander Guyer Aug 04 '23 at 20:02
5

The usual implementation of RVO is that the calling code passes the address of a memory chunk where the function should construct its result object.

When the function result is directly an automatic variable that is not a formal argument, that that local variable can simply be placed in the caller-provided memory chunk, and the return statement then does no copying at all.

For an argument passed by value the calling machine code has to copy-initialize its actual argument into the formal argument’s location before jumping to the function. For the function to place its result there it would have to destroy the formal argument object first, which has some tricky special cases (e.g., when that construction directly or indirectly refers to the formal argument object). So, instead of identifying the result location with the formal argument location, an optimization here logically has to use a separate called-provided memory chunk for the function result.

However, a function result that is not passed in a register is normally provided by the caller. I.e., what one could reasonably talk about as RVO, a kind of diminished RVO, for the case of a return expression that denotes a formal argument, is what would happen anyway. And it does not fit with the text “by constructing the automatic object directly into the function’s return value”.

Summing up, the data flow requiring that the caller passes in a value, means that it is necessarily the caller that initializes a formal argument's storage, and not the function. Hence, copying back from a formal argument can not be avoided in general (that weasel term covers the special cases where the compiler can do very special things, in particular for inlined machine code). However, it is the function that initializes any other local automatic object’s storage, and then it’s no problem to do RVO.

Cheers and hth. - Alf
  • 142,714
  • 15
  • 209
  • 331
  • 4
    While it is true that the common implementation wouldn't support that optimization anyway, this by itself doesn't explain why the standard forbids it. After all, it's an optimization, and therefore not *required* to be done. – celtschk Feb 25 '12 at 15:04
  • @celtschk it's not just the common implementation, but *all* existing implementations. Also note that this optimization cannot be done inside the function, but would have to be performed by the caller with support from the calling conventions. Note that it is the caller the only one that knows that the argument is an *rvalue* and that the space can be reused. Also note that caller and callee would have to agree in a per-call basis on whether the argument would be *semantically* destroyed during the function call or if it is to destroyed later. – David Rodríguez - dribeas Feb 25 '12 at 15:48
  • ... The problem is that allowing that optimization is not giving freedom to the compiler, but rather imposing a whole set of requirements to manage the possibility that the optimization has taken place without causing undefined behavior. Even if it would be up to the compiler to choose that poison, the posibility of defining a module interface was discussed in the standardization process, and left for a later version of the standard. If this optimization was allowed and a single compiler decided to do it, in c++ with modules all compilers would have to bite the bullet. – David Rodríguez - dribeas Feb 25 '12 at 15:51
  • 3
    @DavidRodríguez-dribeas: It doesn't even matter whether *no* current compiler is implemented in a way that permits this optimization. A compiler writer is free to ignore that this optimization is allowed. The compiler writer also knows that this optimization didn't take place in that case because it would have been the compiler which did it, and the compiler didn't. – celtschk Feb 25 '12 at 15:57
  • ... Considering that it cannot be implemented nowadays, the small value that it adds (if in your application it makes sense, just pass by reference, or *move* which is meant to be a light operation) the sensible thing to do is not allowing it at all. – David Rodríguez - dribeas Feb 25 '12 at 15:58
  • @celtschk you might have missed some of the details I the comments... A compiler processing the function on it self (separate compilation model) cannot possibly know whether the copy can be elided. That knowledge lays in the *caller* not the function. The copy from argument to return statement is responsibility of the function, but the knowdledge of its applicability is not there. There is no way to implement this in a language with a separate compilation model. – David Rodríguez - dribeas Feb 25 '12 at 16:03
  • 2
    @DavidRodríguez-dribeas: If it were impossible to implement, there would be even *less* reason for the standard to explicitly(!) forbid it. It would not be necessary to do so, and the extra parenthesis would just be a waste of space and time. If you explicitly forbid something, then you do so because it *would* be possible, but would have consequences you do not want. – celtschk Feb 25 '12 at 17:07
  • @celtschk, the standards aren't written in a vacuum, they're written by the same people who write the compilers. They're aware of the issues. By putting it in the standard it informs us peons with some useful knowledge. If somebody figures out some magic way to do it, they can always introduce a change in the next version of the standard. – Mark Ransom May 02 '14 at 23:13