Sunday, November 11, 2012

On the Superfluousness of std::move

During my presentation of "Universal References in C++11" at C++ and Beyond 2012, I gave the advice to apply std::move to rvalue reference parameters and std::forward to universal reference parameters.  In this post, I'll follow the convention I introduced in that talk of using RRef for "rvalue reference" and URef for "universal reference."

Shortly after I gave the advice mentioned above, an attendee asked what would happen if std::forward were applied to an RRef instead of std::move.  The question took me by surprise.  I was so accustomed to the RRef-implies-std::move and URef-implies-std::forward convention, I had not thought through the implications of other possibilities.  The answer I offered was that I wasn't sure what would happen, but I didn't really care, because even if using std::forward with an RRef would work, it would be unidiomatic and hence potentially confusing to readers of the code.

The question has since been repeated on stackoverflow, and I've also received it from attendees of other recent presentations I've given.  It's apparently one of those obvious questions I simply hadn't considered.  It's time I did.

std::move unconditionally casts its argument to an rvalue.  Its implementation is far from transparent, but the pseudocode is simple:
  
  // pseudocode for for std::move
  template<typename T>
  T&& std::move(T&& obj)
  { 
    return (T&&)obj;        // return obj as an rvalue
  }
std::forward is different.  It casts its argument, which is assumed to be a reference to a deduced type, to an rvalue only if the object to which the reference is bound is an rvalue. (Yes, that's a mouthful, but that's what std::forward does.)  Whether the object to which the reference is bound is an rvalue is determined by the deduced type.  If the deduced type is a reference, the referred-to object is an lvalue.  If the deduced type is a non-reference, the referred-to object is an rvalue.  (This explanation assumes a lot of background on how type deduction works for universal reference parameters, but that's covered in the talk as well as in its printed manifestations in Overload and at ISOcpp.org.)

As with std::move, std::forward's implementation is rather opaque, but the pseudocode isn't too bad:
  // pseudocode for for std::forward
  template<typename T>
  T&& std::forward(T&& obj)
  { 
    if (T is a reference)
      return (T&)obj;          // return obj as an lvalue 
    else
      return (T&&)obj;        // return obj as an rvalue 
  }
Even the pseudocode makes sense only when you understand that (1) if T is a reference, it will be an lvalue reference and (2) thanks to reference collapsing, std:forward's return type will turn into T& when T is an lvalue reference.  Again, this is covered in the talk and elsewhere.

Now we can answer the question of what would happen if you used std::forward on an RRef.  Consider a class Widget that offers a move constructor and that contains a std::string data member:
  class Widget {
  public:
    Widget(Widget&& rhs);       // move constructor
  
  private:
    std::string s;
  };
The way you're supposed to implement the move constructor is:
  Widget::Widget(Widget&& rhs)
  : s(std::move(rhs.s))
  {}
Per convention, std::move is applied to the RRef rhs when initializing the std:string.  If we used std::forward, the code would look like this:
  Widget::Widget(Widget&& rhs)
  : s(std::forward<std::string>(rhs.s)
  {}
You can't see it in the pseudocode for std::forward, but even though it's a function template, the functions it generates don't do type deduction.  Preventing such type deduction is one of the things that make std::forward's implementation less than transparent.  Because there is no type deduction with std::forward, the type argument T must be specified in the call.  In contrast, std::move does do type deduction, and that's why in the Widget move constructor, we say "std::move(rhs.s)", but "std::forward<std::string>(rhs.s)".

In the call "std::forward<std::string>(rhs.s)", the type std::string is a non-reference. As a result, std::forward returns its argument as an rvalue, which is exactly what std::move does.  That answers the original question.  If you apply std::forward to an rvalue reference instead of std::move, you get the same result. std::forward on an rvalue reference does the same thing as std::move.

Now, to be fully accurate, this assumes that you follow the rules and pass to std::forward the type of the RRef without its reference-qualifiers.  In the Widget constructor, for example, my analysis assumes that you pass std::forward<std::string>(rhs.s).  If you decide to be a rebel and write the call like this,
  std::forward<std::string&>(rhs.s)
rhs.s would be returned as an lvalue, which is not what std::move does. It also means that the std::string data member in Widget would be copy-initializd instead of move-initialized, which would defeat the purpose of writing a move constructor.

If you decide to be a smart aleck and write this,
  std::forward<std::string&&>(rhs.s)

the reference-collapsing rules will see that you get the same behavior as std::move, but with any luck, your team lead will shift you to development in straight C, where you'll have to content yourself with writing bizarre macros.

Oh, and if you make the mistake of writing the move constructor like this,
  Widget::Widget(Widget&& rhs)
  : s(std::forward<Widget>(rhs.s))
  {}
which, because I'm so used to passing std::forward the type of the function parameter, is what I did when I initially wrote this article,  you'll be casting one type (in this case, a std::string) to some other unrelated type (here, a Widget), and I can only hope the code won't compile.  I find the idea so upsetting, I'm not even going to submit it to a compiler.

Summary time:
  • If you use std::forward with an RRef instead of std::move, and if you pass the correct type to std::forward, the behavior will be the same as std::move.  In this sense, std::move is superfluous.
  • If you use std::forward instead of std::move, you have to pass a type, which opens the door to errors not possible with std::move.
  • Using std::forward requires more typing than std::move and yields source code with more syntactic noise.
  • Using std::forward on an RRef is contrary to established C++11 idiom and contrary to the design of move semantics and perfect forwarding.  It can work, sure, but it's still an anathema.
Scott

Friday, November 2, 2012

"Universal References in C++11" now online at ISOCpp.org

The October Overload article I blogged about earlier, "Universal References in C++11," is now available online at the spanking new ISOCpp.org website. In conjunction with my video presentation at Channel 9 on the same topic, this means you now have your choice of three different ways to experience my pitch for adding "universal reference" to the accepted C++ vocabulary:
  • Overload version: Monochrome PDF and, if you're a subscriber, hardcopy.
  • Channel 9 version: Video of me presenting the information, along with the original presentation materials.
  • ISOCpp.org version: HTML version with syntax-colored code examples and live hyperlinks.
I really think that the idea of universal references helps make C++11's new "&&" syntax a lot easier to understand. If you agree, please use this term and encourage others to do it, too.

Scott