44

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?

Shital Shah
  • 63,284
  • 17
  • 238
  • 185
  • 5
    What if the function returns a reference? The semantics are different, and asking for what you don't want can burn you badly if you get it. – David Schwartz Jun 06 '16 at 03:44
  • I thought above would work for returned references also as long as I want to treat returned value as immutable. – Shital Shah Jun 06 '16 at 03:46
  • 5
    Keeping a reference to a simple type like `int` can be less efficient than using an `int` variable. – Mark Ransom Jun 06 '16 at 03:49
  • Thatnks, that's one disadvantage. So I guess it should be ok for anything but scalers? – Shital Shah Jun 06 '16 at 03:52
  • you never should initialize a variable (no matter const or not) by its own uninitalized value. Sure thats what you wanted to ask about? – dhein Jun 06 '16 at 12:58
  • Is `decltype(auto)` in C++14 a viable alternative? – John McFarlane Jun 07 '16 at 15:12
  • Note that C++17’s “guaranteed copy elision” removes much of the motivation for using `const … &…=…;`, so that we needn’t ask this question and can use references to express the semantics of identity mattering. – Davis Herring Dec 04 '22 at 18:59

4 Answers4

37

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
30

These have semantic differences and if you ask for something other than you want, you will be in trouble if you get it. Consider this code:

#include <stdio.h>

class Bar
{
    public:
    Bar() { printf ("Bar::Bar\n"); }
    ~Bar() { printf ("Bar::~Bar\n"); }
    Bar(const Bar&) { printf("Bar::Bar(const Bar&)\n"); }
    void baz() const { printf("Bar::Baz\n"); }
};

class Foo
{
    Bar bar;

    public:
    Bar& getBar () { return bar; }
    Foo() { }
};

int main()
{
    printf("This is safe:\n");
    {
        Foo *x = new Foo();
        const Bar y = x->getBar();
        delete x;
        y.baz();
    }
    printf("\nThis is a disaster:\n");
    {
        Foo *x = new Foo();
        const Bar& y = x->getBar();
        delete x;
        y.baz();
    }
    return 0;
}

Output is:

This is safe:
Bar::Bar
Bar::Bar(const Bar&)
Bar::~Bar
Bar::Baz
Bar::~Bar

This is a disaster:
Bar::Bar
Bar::~Bar
Bar::Baz

Notice we call Bar::Baz after the Bar is destroyed. Oops.

Ask for what you want, that way you're not screwed if you get what you ask for.

David Schwartz
  • 179,497
  • 17
  • 214
  • 278
  • why is the code in the snippet so different from the linked code? – Oebele Jun 06 '16 at 09:44
  • Could you explain plz what is here the problem? As I udnerstand this correct, this problem also remains a problem when OP wouldn't declare it const, or what am I missing? – dhein Jun 06 '16 at 13:05
  • 3
    @Zaibis: The problem is the reference, not the `const`. – user2357112 Jun 06 '16 at 16:08
  • @user2357112: Ok then I understand the example, but I don't understand why it is acepted as answer for problems about using allways `const`? – dhein Jun 07 '16 at 06:08
  • 1
    @Zaibis: The question is about whether to use a reference, not whether to use const. See how in the question, the two alternatives proposed are assigning to `const Foo&` and `const Foo`, differing in the use of a reference, rather than the use of const. – user2357112 Jun 07 '16 at 06:50
  • 3
    This appears to be answering a different question to what was originally asked. This is describing what happens when you define a function returning a reference (or not). The question is about _using_ the function and assigning its return value to a reference (or not). – Steve Folly Jun 21 '17 at 10:11
  • @SteveFolly I don't follow. The example shows two different ways to use the function `getBar`, one assigning its return value to a reference and one not. – David Schwartz Jun 21 '17 at 15:29
  • 2
    @DavidSchwartz - the question's example is that the method returns by _value_ . Your example method returns by reference, thus your second example will be a disaster. If Foo::getBar returns by value, your second example won't be a disaster. (Hence, that's why I said you weren't answering the original question.) – Steve Folly Jul 10 '17 at 21:06
14

Building on what @David Schwartz said in the comments, you need to be sure the semantics don't change. It isn't enough that you intend to treat the value as immutable, the function you got it from should treat it as immutable too or you're going to get a surprise.

image.SetPixel(x, y, white_pixel);
const Pixel &pix = image.GetPixel(x, y);
image.SetPixel(x, y, black_pixel);
cout << pix;
Community
  • 1
  • 1
Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
4

The semantic difference between const C& and const C in the considered case (when selecting the type for a variable) can affect your program in the cases listed below. They must be taken into account not only when writing new code, but also during subsequent maintenance, since certain changes to the source code may change where a variable definition belongs in this classification.

Initializer is an lvalue of exactly type C

const C& foo();
const C  a = foo(); // (1)
const C& b = foo(); // (2)

(1) introduces an independent object (to an extent allowed by the copy semantics of the type C), whereas (2) creates an alias to another object and is subject to all changes happening to that object (including its end-of-life).

Initializer is an lvalue of a type derived from C

struct D : C { ... };
const D& foo();
const C  a = foo(); // (1)
const C& b = foo(); // (2)

(1) is a sliced version of what was returned from foo(). (2) is bound to the derived object and can enjoy the benefits of polymorphic behavior (if any), though at the risk of being bitten by aliasing problems.

Initializer is an rvalue of a type derived from C

struct D : C { ... };
D foo();
const C  a = foo(); // (1)
const C& b = foo(); // (2)

For (1), this is no different from the previous case. Regarding (2), there is no more aliasing! The constant reference is bound to the temporary of derived type, whose lifetime extends to the end of the enclosing scope, with the correct destructor (~D()) automatically being called. (2) can enjoy the benefits of polymorphism, but pays the price of the extra resources consumed by D compared to C.

Initializer is an rvalue of a type convertible to an lvalue of type C

struct B {
    C c;
    operator const C& () const { return c; }
};
const B foo();
const C  a = foo(); // (1)
const C& b = foo(); // (2)

(1) makes its copy and goes on, while (2) is in trouble starting immediately from the next statement, since it aliases a sub-object of a dead object!

Community
  • 1
  • 1
Leon
  • 31,443
  • 4
  • 72
  • 97