Why not always assign return values to const reference?

Calling elision an “optimization” is a misconception. Compilers are permitted not to do it, but they are also permitted to implement a+b integer addition as a sequence of bitwise operations with manual carry.

A compiler which did that would be hostile: so too a compiler that refuses to elide.

Elision is not like “other” optimizations, as those rely on the as-if rule (behaviour may change so long as it behaves as-if the standard dictates). Elision may change the behaviour of the code.

As to why using const & or even rvalue && is a bad idea, references are aliases to an object. With either, you do not have a (local) guarantee that the object will not be manipulated elsewhere. In fact, if the function returns a &, const& or &&, the object must exist elsewhere with another identity in practice. So your “local” value is instead a reference to some unknown distant state: this makes the local behaviour difficult to reason about.

Values, on the other hand, cannot be aliased. You can form such aliases after creation, but a const local value cannot be modified under the standard, even if an alias exists for it.

Reasoning about local objects is easy. Reasoning about distributed objects is hard. References are distributed in type: if you are choosing between a case of reference or value and there is no obvious performance cost to the value, always choose values.

To be concrete:

Foo const& f = GetFoo();

could either be a reference binding to a temporary of type Foo or derived returned from GetFoo(), or a reference bound to something else stored within GetFoo(). We cannot tell from that line.

Foo const& GetFoo();

vs

Foo GetFoo();

make f have different meanings, in effect.

Foo f = GetFoo();

always creates a copy. Nothing that does not modify “through” f will modify f (unless its ctor passed a pointer to itself to someone else, of course).

If we have

const Foo f = GetFoo();

we even have the guarantee that modifying (non-mutable parts of) f is undefined behavior. We can assume f is immutable, and in fact the compiler will do so.

In the const Foo& case, modifying f can be defined behavior if the underlying storage was non-const. So we cannot assume f is immutable, and the compiler will only assume it is immutable if it can examine all code that has validly-derived pointers or references to f and determine that none of them mutate it (even if you just pass around const Foo&, if the original object was a non-const Foo, it is legal to const_cast<Foo&> and modify it).

In short, don’t premature pessimize and assume elision “won’t happen”. There are very few current compilers that won’t elide without explicity turning it off, and you almost certainly won’t be building a serious project on them.

Leave a Comment