jdi jdi - 3 months ago 13
C++ Question

Does const ref lvalue to non-const return value imply a copy-free effect?

A colleague of mine has a C++ habit that I have tried to research in order to understand its impact and validate its usage. But I can't seem to find the exact answer.

std::vector< Thing > getThings();

void do() {
const std::vector< Thing > &things = getThings();
}


Here we have some function that returns a non-
const&
value. The habit I am seeing is the usage of a
const&
lvalue when assigning the return value from the function. The proposed reasoning for this habit is that it prevents a copy.

Now I have been researching RVO (Return Value Optimization), copy elision, and C++11 move semantics. I realize that a given compiler could choose to prevent a copy via RVO regardless of the use of
const&
here. But does the usage of a
const&
lvalue here have any kind of effect on non-
const&
return values in terms of preventing copies? And I am specifically asking about pre-C++11 compilers, before move semantics.

My assumption is that either the compiler implements RVO or it does not, and that saying the lvalue should be
const&
doesn't hint or force a copy-free situation.

Edit

I am specifically asking about whether
const&
usage here prevents a copy, and not about the lifetime of the temporary object, as described in "the most important const"

Further clarification of question

Is this:

const std::vector< Thing > &things = getThings();


any different than this:

std::vector< Thing > things = getThings();


in terms of preventing copies? Or does it not have any influence on whether the compiler can reduce copies, such as via RVO?

Answer

Semantically, the compiler needs an accessible copy-constructor, at the call site, even if later on, the compiler elides the call to the copy-constructor — that optimization is done later in the compilation phase after the semantic-analysis phase.

After reading your comments, I think I understand your question better. Now let me answer it in detail.

Imagine that the function has this return statement:

return items;

Semantically speaking, the compiler needs an accessible copy-constructor (or move-constructor) here, which can be elided. However, just for the sake of argument, assume that it makes a copy here and the copy is stored in __temp_items which I expressed this as:

__temp_items <= return items; //first copy: 

Now at the call site, assume that you have not used const &, so it becomes this:

std::vector<Thing> things = __temp_items;  //second copy

Now as you can see yourself, there are two copies. Compilers are allowed to elide both of them.

However, your actual code uses const &, so it becomes this:

const std::vector<Thing> & things = __temp_items;  //no copy anymore.

Now, semantically there is only one copy, which can still be elided by the compiler. As for the second copy, I wont say const& "prevented" it in the sense that compiler has optimised it, rather it is not allowed by the language to begin with.


But interestingly, no matter how many times the compiler makes copies while returning, or elides few (or all) of them, the return value is a temporary. If that is so, then how does binding to a temporary work? If that is also your question (now I know that is not your question but then keep it that way so that I dont have to erase this part of my answer), then yes it works and that is guaranteed by the language.

As explained in the article the most imporant const in very detail, that if a const reference binds to a temporary, then the lifetime of the temporary is extended till the scope of the reference, and it is irrespective of the type of the object.

In C++11, there is another way to extend the lifetime of a temporary, which is rvalue-reference:

std::vector<Thing> && things = getThings();    

It has the same effect, but the advantage (or disadvantage — depends on the context) is that you can also modify the content.

I personally prefer to write this as:

auto && things = getThings();   

but then that is not necessarily a rvalue-reference — if you change the return type of the function, to return a reference, then things turns out to bind to lvalue-reference. If you want to discuss that, then that is a whole different topic.