Last Update:
Const, Move and RVO
Table of Contents
C++ is a surprising language. Sometimes, simple things are more complex in practice. In my article about “use const all the 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. Let’s revise this topic today.
Does a const
variable influence move and Return Value Optimization?
I wrote this post in 2017 and updated it in October 2023 with links and corrected advice.
Intro
To recall, we’re talking here about using const
for variables inside function bodies. It is 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 constant.
What’s the problem?
First of all, you cannot move from an object that is marked as const
.
Another potential problem is when a compiler tries 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 the problem with move and RVO.
Move semantics
Move semantics (see this excellent 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 somewhat 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, the move semantics usually works on temporary objects, so you won’t even see them. Even if you have some constant things, 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, the 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 lvalue, 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 need such an 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 slightly altered in that context.
Returning a const object
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, we must perform a standard copy.
For example:
MyType ProduceType(int a) {
cout << "ProduceType\n";
MyType t;
t.mVal = a;
return t;
}
MyType ProduceTypeWithConst(int a) {
const MyType t = ProduceType(a);
return t;
}
int main() {
MyType t = ProduceTypeWithConst(1);
}
What’s the expected output here?
For sure, two objects need to be created t
and one object inside the functions.
Compiling with -std=c++17 -Wall -fno-elide-constructors
, run @Compiler Explorer
ProduceTypeWithConst
ProduceType
MyType()
MyType(MyType&& v)
~MyType()
MyType(const MyType&)
~MyType()
~MyType()
As you can see, marking the return object as const
causes to disable move and calls the copy constructor.
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.
MyType ProduceType() {
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 a named const
object
What happens if your object is const
? Like:
MyType ProduceType(int a) {
cout << "ProduceType\n";
MyType t;
t.mVal = a;
return t;
}
MyType ProduceTypeWithConst(int a) {
cout << "ProduceTypeWithConst\n";
const MyType t = ProduceType(a);
return t;
}
int main() {
MyType t = ProduceTypeWithConst(1);
}
Can RVO be applied here? The answer is Yes.
See it @Compiler Explorer:
The output:
ProduceTypeWithConst
ProduceType
MyType()
~MyType()
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.
NRVO and moveable only objects
One reader (thanks Karel!) also pointed out, that since technically a copy constructor is needed even for NRVO, you cannot return const
objects like smart pointers:
std::unique_ptr<MyType> ProduceTypeWithConstPtr(int a) {
cout << "ProduceTypeWithConstPtr\n";
const auto t = std::make_unique<MyType>(ProduceType(a));
return t;
}
int main() {
auto t = ProduceTypeWithConstPtr(1);
}
This time, the move operation cannot be invoked, so the compiler returns an error:
<source>:31:12: error: use of deleted function 'std::unique_ptr<_Tp, _Dp>
::unique_ptr(const std::unique_ptr<_Tp, _Dp>&)
[with _Tp = MyType; _Dp = std::default_delete<MyType>]'
31 | return t;
| ^
The slightly altered advice
In function bodies:
- Use
const
whenever possible.
Exceptions:
- Assuming the type is movable, when you want to move explicitly such a 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 it prevents (very frequent) use of move semantics.
Summary
- While
const
is generally your friend, slapping it everywhere may not always be the best choice, especially when you’re looking for those performance gains from move semantics. - You can’t move from a
const
object, period. This can be a hit on performance if the type you’re working with benefits from move semantics. - Good news is,
const
doesn’t impact Return Value Optimization (RVO). Compilers are smart enough to handle that. - Core Guidelines exist for a reason. While general advice leans towards using
const
often, there are specific scenarios where you should think twice.
You can play with the code here: @coliru.
Back to you
- When do you use
const
? - In what situations you’re afraid to use it?
I've prepared a valuable bonus if you're interested in Modern C++!
Learn all major features of recent C++ Standards!
Check it out here: