Let's say I have some function:
Foo GetFoo(..) { ... }
Assume that we neither know how this function is implemented nor the internals of Foo (it can be very complex object, for example). However we do know that function is returning Foo by value and that we want to use this return value as const.
Question: Would it be always a good idea to store return value of this function as const &
?
const Foo& f = GetFoo(...);
instead of,
const Foo f = GetFoo(...);
I know that compilers would do return value optimizations and may be move the object instead of copying it so in the end const &
might not have any advantages. However my question is, are there any disadvantages? Why shouldn't I just develop muscle memory to always use const &
to store return values given that I don't have to rely on compiler optimizations and the fact that even move operation can be expensive for complex objects.
Stretching this to extreme, why shouldn't I always use const &
for all variables that are immutable in my code? For example,
const int& a = 2; const int& b = 2; const int& c = c + d;
Besides being more verbose, are there any disadvantages?
You want to return a const reference when you return a property of an object, that you want not to be modified out-side of it. For example: when your object has a name, you can make following method const std::string& get_name(){ return name; }; . Which is most optimal way.
Then, no, you can't do that; in a const method you have a const this pointer (in your case it would be a const Foo * ), which means that any reference you can get to its fields1 will be a const reference, since you're accessing them through a " const path".
A non-const reference cannot point to a literal. You cannot bind a literal to a reference to non-const (because modifying the value of a literal is not an operation that makes sense) and only l-values can be bound to references to non-const.
You are not questioning why const references are allowed to bind to temporaries, but merely why they extend the lifetime of those temporaries. If the lifetime of the temporary returned by bar() were not extended, then any usage of a (exemplified by the line (1)) would lead to undefined behavior.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With