C++ curiosities: std::move that doesn’t move

by Max Galkin

Despite its name, std::move doesn’t move anything. std::move is no more than a type cast. It has 2 major purposes in life: to return an “rvalue reference” to its argument and to mislead people into thinking it does more than that. In fact, you can call the bluff by calling static_cast<T&&>¬†explicitly instead of std::move and observe all move operations still working to the same effect:

  #include <iostream>
  using namespace std;

  struct A 
  {
    A(){};
    A(A&&) { cout << "I'm so moved!" << endl; };
  };

  int main() 
  {
    A a;
    A aa(static_cast<A&&>(a)); // prints "I'm so moved!"
  }

Why does it matter? Because developers tend to forget about the fact, and treat calls to std::move as some kind of reset() method on the object, and that can lead to defects and fragile code.

There are several reasons for which a move operation is not a “reset”:

  1. As shown above, std::move itself doesn’t change the given object in any way, it just returns a “special” reference to it.
  2. Whoever receives the “special” reference has no obligation to really move the data out of the referenced location (even though they usually do).
  3. Even if the “special” reference is consumed in a real move operation, in general, there is no guarantee that the object will be reset, or left in any particular state whatsoever after that. It may very well contain garbage. Move operations on C++ standard library objects promise to leave them in a valid but unspecified state. It’s probably a good idea to follow the same contract for user-defined move operations, and document any deviations explicitly.
  #include <iostream>
  #include <string>
  using namespace std;

  // Some function X.
  // It takes an rvalue ref to s, but doesn't move data out of it.
  // It promises to return "true", if it ever moves out of "s". 
  bool X(string&& s)
  {
    s.pop_back();
    return false;
  }

  // Some function Y.
  // It moves out of "s" and then dumps garbage into it.
  // Because it can.
  void Y(string&& s)
  {
    string local_string = std::move(s);

    s = "Muahaha! Garbage!";
  }

  int main() 
  {
    string s("Hello, world!");
    string&& rvalue_ref_to_s = std::move(s);

    cout << "1. std::move by itself doesn't change the string:" << endl;
    cout << s << endl; // prints "Hello, world!"

    bool wasMoved = X(std::move(s));
    if (!wasMoved)
    {
      cout << "2. We know that data wasn't moved out of ""s""," << endl;
      cout << "   because such is the contract of X(...)," << endl;
      cout << "   we can continue working with ""s"" as usual" << endl;
      cout << s << endl; // prints "Hello, world"
    }
    else return 0;

    Y(std::move(s));
    
    // 3. Data was moved out of "s", but it is not empty.
    // If we want to continue using "s", we should reset it,
    // or assign something to it.
    s = "The New World";


    string s2(std::move(s));

    // 4. You might think that now "s" is definitely empty,
    // but the standard only promises that "s" is in a
    // "valid but unspecified" state, so you should only call
    // methods without preconditions to remain within
    // the bounds of the specified behavior.
    s.clear();
  }

To summarize: std::move doesn’t move a thing, it just creates an “open-minded” reference which lets you move data out of it, but doesn’t require you to do so. The actual move happens (or not) later on during the lifetime of that reference. In general, after a move operation happens, the state of a moved-from object is unspecified. The safest best practice you can follow is to never reuse the moved-from objects in the first place. But, if you’re determined to reuse a moved-from object, reset it explicitly.