Move Semantics in C++: Unraveling the Myth of Always Passing by Const-Reference

Context

Lately I’ve been giving advanced C++ lectures at DigiPen Institute of Technology Europe – Bilbao. This included teaching C++11 features such as move semantics. At this time I realized that, when someone is learning C++, one of the first things we tell them is that all complex objects like containers (vectors, sets, maps…) should be passed by const-reference, instead of by value, to avoid unnecessary copies. Students missed the const-ref many times and passed everything by value, which is wrong most of the times. Thus,I had to keep repeating myself telling them to get used to the general rule of passing objects by const-ref.

Students at some point get the idea and start passing everything by const-ref, which is great, isn’t it? Well… It turns out it’s not that great. I realized this when I was explaining move semantics. The problem is that, since move-semantics were introduced in C++11, the general rule of “pass everything by ref” is no longer true and, in some cases, the best is to pass elements by value.

Always pass parameters to functions as const-reference

This needs to be unlearned

This doesn’t just apply to students

I’ve stumbled with the pattern I describe below several times along my career, in code reviews and code-bases. In all those cases, I had to give the same explanation I’m giving below. I believe that there’s more people that might benefit from this explanation and that’s why I decided to write this blog post.

The Problem

Here’s the case we want to avoid. We don’t need to overload the function with an r-value reference, that leads to code duplication which means we’ll end up with two functions to maintain, although sometimes those functions are small I’ve seen this pattern with relatively big functions. We could just implement one function for this, and that’s what we’ll be working towards in this post.

    // NOT IDEAL scenario, you shouldn't need these overloads
    Team make_team(const std::vector<Player*> & players);
    Team make_team(std::vector<Player*> && players);
  

Disclaimer

This post doesn’t talk about move constructors or move assignment operator. Those are absolutely needed and all classes that manage resources should follow the rule of five or zero, implementing all the operations they require. This post covers the usage of r-value references on functions that are not the move constructor or the move assignment operator.

This post also assumes you understand what move semantics are and what they are used for (i.e.: to avoid unnecessary copies).

How do we end up with those overloads? (My guess)

In order to understand a solution to a problem, we first need to understand the problem and why we reached that problematic state. In this case, we have to ask ourselves: “Why did someone end up overloading this function with an r-value reference?”. To understand this, we have to understand how people programmed before and after move semantics were introduced. This means before and after C++11.

Programming without move semantics

Here’s the example we’ll be working with: imagine we have a Team structure that takes as input an array of Players that belong to this Team. Before move semantics, this is how we would implement this structure: we take the parameter by const-ref and there’s just one copy made. This code below is our code, the interface implementation.

    // Our code
    struct Team
    {
        Team(const std::vector<Player*> & players)
            : m_players(players) // Copy happens here
        {}

        std::vector<Player*> m_players;
    };
  

Below there’s an example of how this code would be used, this is the user code. In this case we are passing the array by const-ref and then we are using the players array. You might be wondering Why are we using the array? That’s a great question  We are using it because we can. In this code we end up having two instances of the players array: the local players variable and the copied array into Team::m_players.

    // User code (example 1)
    void func()
    {
        std::vector<Players*> players = get_all_players();
        
        Team A(players); // Passed by const-ref, no copy made

        // Do something with Team A
        // ...

        // Here we could also do something with the players...
        for(size_t i = 0; i < players.size(); ++i)
            std::cout << players[i]->name() << std::endl;
    }
  

Now, a small change to the code above that’s important moving forward: imagine we didn’t have the final for loop printing the player names. This would mean we would end up having two player array instances at the same time, while having just one would have been enough.

    // User code (example 2)
    void func()
    {
        std::vector<Players*> players = get_all_players();
        
        Team A(players); // Passed by const-ref, no copy made

        // Do something with Team A
        // ...

        // We don't use the local players array anymore, that's a bit wasteful
    }
  

Programming with move semantics

After move semantics were introduced in C++11, we can use r-value references to improve example 2. I think this is the reason why people implement the overloaded function that’s really not needed. They already have a function taking a const-ref and then they add an overloading an r-value reference. The code ends up like this:

    // Our code
    struct Team
    {
        // NOT IDEAL: we have two functions to maintain!
        Team(const std::vector<Players*> & players)
            : m_players(players) // Copy
        {
                // Potential duplicated code
        }
        Team(std::vector<Players*> && players)
            : m_players(std::move(players)) // Move, no copy made
        {
                // Potential duplicated code
        }

        std::vector<Players*> m_players;
    };
  

This overload is added with good intention to avoid creating an extra copy of the vector. In some cases, like the example 2 above where we don’t want to use the local variable, we could just move it into Team A. When the r-value ref overload is added the user code can be updated, as shown below, to move the players array.

    // User code (example 3)
    void func()
    {
        std::vector<Players*> players = get_all_players();
        
        // Call ctor taking an r-value vector, we only created one vector
        Team A(std::move(players));

        // Do something with Team A
        // ...

        // players is empty at this point, no problem because we don't use it
    }
  

If we still wanted to use the players array after constructing Team A (as shown in example 1, to print the names, for instance), then we wouldn’t do the std::move and the const-ref overload would be called instead. We have one function for each use-case.

You might be thinking: But you said the r-value reference overload is not required! You just explained that it is useful!. Yes, I know, it’s useful in the way I described above, but that doesn’t mean this is the best approach. We have two functions to maintain and in most cases the body of the functions has code that’s copy/pasted! Remember that we were trying to understand why people end up implementing this overload. Now we know why it happens and we have identified some use-cases.

The two use-cases

  • Be able to still use our local variable (as seen in example 1)
    • In this case we passed players by const-ref and then we were able to use the array to print the names of the players because it still contained elements.
  • Be able to move our local variable into the calling function in cases where we don’t want to sue our variable anymore, to avoid having extra copies (as seen in example 3)
    • Notice that the code in example 2 is not using the players array but this code is not ideal since we still create one additional players array.

The ideal implementation

Finally, the grand finale! Like any good movie, the end of this blog also has an unexpected ending. The ideal implementation using just one function is by taking the parameter by value This works for both cases described above. The code below shows how the function should be implemented.

    // Our code
    struct Team
    {
        // IDEAL: just one function to maintain, works for all cases
        Team(std::vector<Players*> players)
            : m_players(std::move(players)) // Always move
        {}

        std::vector<Players*> m_players;
    };
  

Now we’ll see how this code covers both cases we described. The snippets below use the same code as examples 1 and 3, just with different comments to explain how they work with the ideal implementation that only uses one function taking the parameter by value.

Firstly the case where we want to still use our local variable in the function:

    // User code (same code as example 1, different explanatory comment)
    void func()
    {
        std::vector<Players*> players = get_all_players();
        
        // players will be copied into the ctor parameter and then moved into A.m_players,
        // we end up having two instances of the players array at once but in this
        // case is required because we still want to use players in this function
        Team A(players);

        // Do something with Team A
        // ...

        // Here we could also do something with the players...
        for(size_t i = 0; i < players.size(); ++i)
            std::cout << players[i]->name() << std::endl;
    }
  

Secondly, when we don’t want to use the local variable in our function anymore:

    // User code (same code as example 3, different explanatory comment)
    void func()
    {
        std::vector<Players*> players = get_all_players();
        
        // Move our local variable into the function parameter,
        // note that this will be moved again later, we end up with just 1 vector
        Team A(std::move(players));

        // Do something with Team A
        // ...
    }
  

Final notes

Note that in the examples above I’ve used a conversion constructor, but I could have also used a normal function like below. All the explanations in this post are also valid with these functions:

    // NOT IDEAL scenario, you shouldn't need these overloads
    Team make_team(const std::vector<Player*> & players);
    Team make_team(std::vector<Player*> && players);
  

And the correct version would be as such:

    // IDEAL scenario, just one function that takes the parameter by value
    Team make_team( std::vector<Player*> players);

If you still have questions about how this would apply with a normal function just replace this on all examples:

  • Team A( players ); with Team A = make_team( players );
  • Team A( std::move(players) ); with Team A = make_team( std::move( players ) );

There is one case when the overload is useful

Okay, yes, there is one case when overloading a function using the r-value ref it’s useful. The reason I didn’t mention it until the end is because I’ve never actually seen it used like this. Every time I saw the overload it wasn’t required, I think 90% of the times people make the wrong overload and maybe the other 10% of the times the overload is required.

The case where the overload is useful is when the complex object is not always used (e.g. some conditional inside the function determines if we want to use it or not). See the example below:

    Team make_team(const std::vector<Player*>& players, bool use_players)
    {
        // Some condition that determines whether players is used or not
        if( use_players )
            return Team( players );

        // Some other logic that doesn't require the players array
        return Team( ... );
    }
  

In this case, if the function would take the parameter players by value, we would be making a copy that’s not used when use_players equals false. Of course in this example the caller of the function could just check use_players before calling and avoid creating the players array altogether… but this is just an example and the point is that the condition in use_players could be any kind of complex condition, making it not obvious when the array would be used and when not.

In this case, we could have an overload where the function that takes players by reference will just copy the array. The overload that takes the parameter by r-value reference would move it. Here we have the r-value overload for the above function:

    // r-value reference overload for the above function
    Team make_team(std::vector<Player*> && players, bool use_players)
    {
        // Some condition that determines whether players is used or not
        if( use_players )
            return Team(std::move(players));

        // Some other logic that doesn't require the players array
        return Team( ... );
    }
  

Still, even if in this case it makes sense to use the r-value overload. Notice that we’ve duplicated the code inside the function and now we have two functions to maintain. The only difference between the functions is the line containing the std::move. The rest should probably remain exactly the same in both functions. In this case, I would personally prefer just having one function that takes a const reference to avoid having duplicated code. 

If the overhead of making an additional copy is too big, I would argue the function should be redesigned. If it’s too expensive to make an additional copy, then it’s also too expensive to create the players array to then discard it. Why are we creating the players array and passing it to the function if then we’ll discard it? We shouldn’t create the players array in the first place.

The kind of situations where using the overload is a correct option are very specific to what you are programming, so feel free to evaluate and decide if you want to use it or not. My rule of thumb is to have just one function and either take it by value or by const reference, I don’t like duplicated code and in most cases the performance impact that the decision we make in these cases is negligible, therefore not worth the trouble of analyzing what option is best. My goal with this post was to explain the case where the overload is not required.

Conclusion

Most people add an r-value overload with the best of intentions. They want to avoid having two copies of an object when one is enough. The problem is that they fail on the execution, since creating an overload leads to more code that will need to be maintained over the years.

I think the ideal case (i.e. having just one function that takes the object by value) goes unnoticed because we learned that we should always use const-references. Therefore, this is the first function that gets created, then the r-value overload gets added. The function that takes the object by value is never considered because we learned that was the “wrong” approach.

Sometimes learning means unlearning something first: whether  completely or partially. In this case, we have to partially unlearn that const-references are still what we should use in most cases.

If you have any questions regarding when to overload a function using r-value reference parameters or you have any other topic you think is worth sharing, let me know in the comments below or contact me here.

I hope this post was useful! 🙂

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Create a website or blog at WordPress.com

Up ↑