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:

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;
      |            ^

See @Compiler Explorer

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:  

Core Guidelines, F.20:

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?