C++ is a surprising language. Sometimes simple things are not that simple in practice. Last time I argued that in function bodies const
should be used most of the time. But two cases were missed: when moving and when returning a value.
Does const
influence move and RVO?
Intro
Just to recall, we’re talking here about using const
for variables inside function bodies. Not about const
for a return type, const
input parameters, or const
methods. In example:
Z foo(T t, X x)
{
const Y y = superFunc(t, x);
const Z z = compute(y);
return z;
}
In the code above it’s best when y
and z
are declared as constant.
So what’s the problem then?
First of all, you cannot move from an object that is marked as const
.
Another potential problem is when a compiler is trying to use (Named) Return Value Optimization (NRVO or RVO). Can it work when the variable to be elided is constant?
I got the following comment from u/sumo952:
Expert #1: “Put const on every variable that does not change. It’s good practice, prevents you from mistakes (changing a variable you intended to be const), and if you’re lucky, the compiler might be able to optimize better.”
Expert #2: “You cannot move from a variable marked as const, and instead the copy-constructor/assignment will be invoked more often. So spraying const-glitter all over your variables may do you more harm than good.”
Great! Now I got two contradictory expert opinions. And sorry, “Know what you’re doing” doesn’t help.
Let’s try to think about better advice. But first, we need to understand what’s the problem with move and RVO.
Move semantics
Move semantics (see this great post for more: C++ Rvalue References Explained
By Thomas Becker) enables us to implement a more efficient way of copying large objects. While value types need to be copied byte by byte anyway, types like containers, resource handles might sometimes be copied by stealing.
For instance, when you want to ‘move’ from one vector to another instead of copying all the data, you can just exchange pointers to the memory allocated on the heap.
Move operation cannot always be invoked, it’s done on r-value references - objects that are usually temporal, and it’s safe to steal from them.
Here’s some explicit code for move:
a = std::move(b);
// b is now in a valid, but 'empty' state!
In the simple code snippet above if the object a
has a move assignment operator (or a move constructor depending on the situation), we can steal resources from b
.
When b
is marked as const
instead of an r-value reference, we’ll get a const r-value’ reference. This type cannot be passed to move operators, so a standard copy constructor or assignment operator will be invoked. No performance gain!
Note, that there are const
r-values in the language, but their use is rather exotic, see this post for more info if needed: What are const rvalue references good for? and also in CppCon 2014: Stephan Lavavej talk.
OK… but is this really a huge problem for us?
Temporary objects
First of all, most of the time move semantics works on temporary objects, so you won’t even see them. Even if you have some constant objects, the result of some function invocation (like a binary operator) might be something else, and usually not const.
const T a = foo();
const T b = bar();
const T c = a + b;// result is a temp object
// return type for the + operator is usually not marked as const
// BTW: such code is also a subject of RVO... read later...
So, in a typical situation, constness of the objects won’t affect move semantics.
Explicit moves
Another case is when you want to move something explicitly. In other words, you take your variable which is an l-value, and you want to make it as it was an r-value.
The core guideline mentions that we usually shouldn’t often call std::move
explicitly:
ES.56: Write std::move() only when you need to explicitly move an object to another scope
And in the case when you really need such operation I assume you know what you’re doing! Using const
here is not a good idea. So I agree that my advice can be altered a bit in that context.
Returning a value
In the case when copy elision cannot be applied the compiler will try to use a move assignment operator or a move constructor if possible. If those aren’t available, then we have to perform a standard copy.
For example:
MyTypeProduceType(int a)
{
MyType t;
t.mVal = a;
return t;
}
MyTypeProduceTypeWithConst(int a)
{
constMyType t =ProduceType(a);
return t;
}
MyType t;
t =ProduceTypeWithConst(1);
What’s the expected output here? For sure two objects needs to be created t
and one object inside the functions. But when returning from ProduceTypeWithConst
the compiler will try to invoke move if possible.
MyType()
MyType()
operator=(MyType&& v)
~MyType()
~MyType()
As you can see marking the return object as const
didn’t cause any problems to perform a move. It would be a problem only when the function returned a const MyType
, but it returns MyType
so we’re safe here.
So all in all, I don’t see a huge problem with move semantics.
Let’s now move to another topic RVO…
Return Value Optimization
RVO is an optimization performed by most compilers (and mandatory in C++17!). When possible, the compiler won’t create an additional copy for the temporal returned object.
MyTypeProduceType()
{
MyType rt;
// ...
return rt;
}
MyType t =ProduceType();// (N)RVO
The canonical C++ would do something like this in the code above:
- construct
rt
- copy
rt
to a temporary object that will be returned - copy that temporary object into
t
But the compiler can elide those copies and just initialize t
once.
You can read more about (N)RVO in the articles from FluentCpp and Undefined Behaviour.
Returning const
What happens if your object is const
? Like:
MyTypeProduceTypeWithConst(int a)
{
constMyType t =ProduceType(a);
return t;
}
MyType t =ProduceTypeWithConst(1);
Can RVO be applied here? The answer is Yes.
It appears that const
doesn’t do any harm here. What might be the problem is when RVO cannot be invoked, then the next choice is to use move semantics. But we already covered that in the section above.
The slightly altered advice
In function bodies:
Use const
whenever possible. Exceptions:
* Assuming the type is movable, when you want to move explicitly such variable, then adding const
might block move semantics.
Still, if you’re unsure and you’re working with some larger objects (that have move enabled), it’s best to measure measure measure.
Some more guidelines:
The argument for adding const to a return value is that it prevents (very rare) accidental access to a temporary. The argument against is prevents (very frequent) use of move semantics.
Summary
While initially, I was concerned about some negative effects of using const
in the case of move and RVO, I think it’s not that serious. Most of the time the compiler can elide copies and properly manage temporary objects.
You can play with the code here: @coliru.
- Did I miss something?
- In what situations you’re afraid to put const?