Partners: KDAB Whole Tomato Software CppDepend

23 January 2017

Const, Move and RVO

Const Move and RVO

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:

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:

MyType ProduceType(int a)
{
    MyType t;
    t.mVal = a;
    return t;
}

MyType ProduceTypeWithConst(int a)
{
    const MyType 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.

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 const

What happens if your object is const? Like:

MyType ProduceTypeWithConst(int a)
{
    const MyType 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:

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 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?

Get my free ebook about C++17!

More than 50 pages about the new Language Standard.

C++17 in detail, by Bartlomiej Filipek

For now I don't have my own courses, but I promote others :) (Warning: I'll also get a little commission for every signup). Have a look my recommended C++ courses at @Pluralsight (more info in my Resource page):

© 2017, Bartlomiej Filipek, Blogger platform
Any opinions expressed herein are in no way representative of those of my employers.
This site contains ads or referral links, which provide me with a commission. Thank you for your understanding.