C++ curiosities: one does not simply move a const object

by Max Galkin

moveWhen I learned about the C++11 move semantics for the first time, it was mighty confusing to me. My mental model of moving something, as in “move a physical object from point A to point B”, was clearly not an adequate mental model for what C++11 calls “moving an object”. And one of the examples of this inadequacy is the case of const objects. You’d think that it’s perfectly normal to “move” something without changing it, right? I mean, you’re just taking a thing and putting it into another place. The thing itself stays the same!.. But no, to “move” an object in C++ you must be able to change it, because the moving works by breaking into the object, copying some of its internals, and rewiring what’s left in there to change the destruction behavior. Since this is sort of counter-intuitive, bad things can happen if you’re not being careful. I’m showing a couple of real-world examples below.

This post continues the move semantics series: std::move that doesn’t move; move semantics is not about moving; move semantics, sizeof, areaof, and pointy types.

Probably the most trivial example to illustrate the topic is shown below, you might be expecting that x2 would be move-constructed from x1, but it will be copy-constructed (and will print “copy”):

    #include <iostream>
    using namespace std;

    class X
    {
    public:
        X(){};
        X(const X&) { cout << "copy" << endl; };
        X(X&&)      { cout << "move" << endl; };
    };

    int main()
    {
        const X x1;
        X x2 = std::move(x1);
    }

It’s relatively easy to understand what’s going on, std::move(x1) produces a const X&&, which can only be legitimately passed to the copy-constructor of X. And you might be thinking that this example is contrived and would never happen in practice. But consider the following.

First, it could be more difficult to spot in real code. In a larger function the declaration of the variable and the move could be separated by many lines. This could also happen in the move-constructor initialization section, if one of the fields of the class is const and you are attempting to move it. Just be aware of that, when reviewing the changes messing with rvalue references.

Second, and more concerning, is the fact that we have so easily obtained a const rvalue reference, even though there seems to be no good situation to use a const rvalue reference ever. As far as my understanding goes, this is just another C++ way of shooting yourself in the foot. Please tell me if you know, but I really don’t see why std::move should be returning them, because usually they seem to be a result of a programmer’s mistake, not their intent.

(2015-05-16 update. Kudos to David Krauss, who, in a follow-up discussion of this topic, pointed out a real-world example of when this exact behavior of std::move is necessary, see page 3 in his proposal N4166 for movable initializer lists. I’m still considering this a rare corner case usage, but it does exist.)

For example, here is a real case (names of all classes and fields were changed to protect their identities). A couple of weeks ago I was adding move constructors to a few classes, and accidentally in one of them I made a mistake… well, see if you can discover it first in this improvised code review:

   #include <iostream>
   #include <vector>
   using namespace std;

   class VectorWrapper
   {
   private:
      vector<int> m_v;

   public:
      // Constructs wrapper from the given vector.
      VectorWrapper(vector<int>&& v)
         : m_v(std::move(v))
      {
      };

      // Copy construct VectorWrapper.
      VectorWrapper(const VectorWrapper& other)
         : m_v(other.m_v)
      {
         cout << "copy" << endl;
      };

      // Move construct VectorWrapper.
      VectorWrapper(const VectorWrapper&& other)
         : m_v(std::move(other.m_v))
      {
         cout << "move" << endl;
      };
   };

   int main()
   {
      VectorWrapper x1({1, 2, 3});
      VectorWrapper x2 = std::move(x1);
   }

The mistake I’m talking about is in the signature of the move constructor (line 25). It should be taking a non-const rvalue reference, but it takes const X&& just accidentally because of its similarity to copy constructor. And guess what, all the type checks are passing, the program prints “move”, as you’d expect, but the vector m_v is copy-constructed, not move-constructed. Well, I was lucky to spot the problem early, but this kind of mistake can be really sneaky!

In “Effective Modern C++” (which I wholeheartedly recommend), Scott Meyers talks in detail about rvalue references and std::move and one of the ideas he mentions is that, perhaps, std::move should have been called std::rvalue_cast instead, to really make it clear that it is simply a cast. I’d take this idea one step further and add a static assert in there to prevent it from returning const rvalue references:


   template <typename T>
   typename remove_reference<T>::type&&
   rvalue_cast(T&& t)
   {
      static_assert(!std::is_const<typename remove_reference<T>::type>::value,
         "You've attempted a cast to a const rvalue reference. "
         "Make sure you're not trying to move a const object, "
         "as this would likely result in a copy not a move. "
         "If you need it for real, call std::move(...) instead.");
      return std::move(t);
   }

If you’d call this rvalue_cast function instead of the std::move in the above code examples, you’d receive compiler errors pointing to the places where copy-construction happens erroneously instead of the expected move-construction.