When Movable Objects Don’t Move

I was debugging some code the other day and came across a little-known caveat of move semantics. Upon fixing the bug, I explained the issue to a few engineers in the company who believed they were sufficiently well versed in move semantics. They were taken by surprise. Had they never written move assignment operators before? Was movement more deep and mysterious than they could imagine? Well, no. They were more than capable to be relied upon for these sorts of things, but there was one thing no one knew.

Take the following code snippet:

AlsoMovable has an std::vector member named vector. This code compiles successfully, passing the static asserts. AlsoMovable has no explicit constructors, operators, or destructor. Can it be assumed that the vector is moved between objects when that assignment is performed?

If you answered yes, you are mistaken. Though you could hardly be blamed. After all, the compiler just told us that both Movable and AlsoMovable are indeed move assignable! That assignment, using std::move to cast m to an rvalue reference, will surely be a move assignment! However, the vector is actually being copied rather than moved. How is this possible?

Well, let’s take a look at the definition of AlsoMovable:

AlsoMovable, an std::is_move_assignable class, contains a member which is not move assignable. But wait, NotMovable looks move assignable! Take a closer look. For those familiar with the copy+swap idiom, you might be used to seeing a copy assignment operator take in a value rather than a const reference. But you’re not supposed to use such a signature when a move assignment is present. That had been missed when a move assignment was later added to the class.

What does this mean for the implicit move assignment operator on AlsoMovable? It is supposed to do a member-by-member move assignment. But what is the result of nm = std::move(other.nm) ? If you write it explicitly, you get a compile error. It’s an ambiguous call that cannot be resolved between the two assignment operators on NotMovable.

And so? Surely the behavior will be that vector gets moved and nm does not? Nope, C++ just decides that the vector will do a copy! It takes the stance “if I can’t move everything, nothing will move!” without warning. This behavior is consistent across MSVC, Clang, and GCC. Even if NotMovable simply declares outright that it does not want to be move assigned by explicitly deleting the move assignment operator, the vector on AlsoMovable gets copied in that case as well.

While there is seemingly no logical reason for this bizarre behavior and it is inexplicable to me why std::is_move_assignable returns true for AlsoMovable when the program is never going to decide to move assign it, there may be a reason behind the madness. If there is a reason, the behavior is still very misleading; surely there was a better way to go about this. At least, knowing the behavior, we can be wary of it and take a second (and third) look before assuming our members are going to be moved. This issue is a very good argument for unit testing to ensure all your data actually gets moved when you expect it to!