During my presentation of
"Universal References in C++11" at
C++ and Beyond 2012, I gave the advice to apply
Shortly after I gave the advice mentioned above, an attendee asked what would happen if
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.
As with
Now we can answer the question of what would happen if you used
In the call "
Now, to be fully accurate, this assumes that you follow the rules and pass to
If you decide to be a smart aleck and write this,
Oh, and if you make the mistake of writing the move constructor like this,
Summary time:
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
inOverload 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 ofstd::move
, and if you pass the correct type tostd::forward
, the behavior will be the same asstd::move
. In this sense,std::move
is superfluous. - If you use
std::forward
instead ofstd::move
, you have to pass a type, which opens the door to errors not possible withstd::move
. - Using
std::forward
requires more typing thanstd::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
Reference:
http://scottmeyers.blogspot.com/2012/11/on-superfluousness-of-stdmove.html