Consider the effect that has when compilers have to determine which std::string
constructor to call. There are two possibilities:
class string { // std::string is actually a
public: // typedef for std::basic_string
…
string(const string& rhs); // copy ctor
string(string&& rhs); // move ctor
…
};
In the Annotation
constructor's member initialization list, the result of std::move(text)
is an rvalue of type const std::string
. That rvalue can't be passed to std::string
's move constructor, because the move constructor takes an rvalue reference to a non- const std::string
. The rvalue can, however, be passed to the copy constructor, because an lvalue-reference-to- const
is permitted to bind to a const
rvalue. The member initialization therefore invokes the copy constructor in std::string
, even though text
has been cast to an rvalue! Such behavior is essential to maintaining const
-correctness. Moving a value out of an object generally modifies the object, so the language should not permit const
objects to be passed to functions (such as move constructors) that could modify them.
There are two lessons to be drawn from this example. First, don't declare objects const
if you want to be able to move from them. Move requests on const
objects are silently transformed into copy operations. Second, std::move
not only doesn't actually move anything, it doesn't even guarantee that the object it's casting will be eligible to be moved. The only thing you know for sure about the result of applying std::move
to an object is that it's an rvalue.
The story for std::forward
is similar to that for std::move
, but whereas std::move
unconditionally casts its argument to an rvalue, std::forward
does it only under certain conditions. std::forward
is a conditional cast. To understand when it casts and when it doesn't, recall how std::forward
is typically used. The most common scenario is a function template taking a universal reference parameter that is to be passed to another function:
void process(const Widget& lvalArg); // process lvalues
void process(Widget&& rvalArg); // process rvalues
template // template that passes
void logAndProcess( T&& param) // param to process
{
auto now = // get current time
std::chrono::system_clock::now();
makeLogEntry("Calling 'process'", now);
process( std::forward(param));
}
Consider two calls to logAndProcess
, one with an lvalue, the other with an rvalue:
Widget w;
logAndProcess(w); // call with lvalue
logAndProcess(std::move(w)); // call with rvalue
Inside logAndProcess
, the parameter param
is passed to the function process
. process
is overloaded for lvalues and rvalues. When we call logAndProcess
with an lvalue, we naturally expect that lvalue to be forwarded to process
as an lvalue, and when we call logAndProcess
with an rvalue, we expect the rvalue overload of process
to be invoked.
But param
, like all function parameters, is an lvalue. Every call to process
inside logAndProcess
will thus want to invoke the lvalue overload for process
. To prevent this, we need a mechanism for param
to be cast to an rvalue if and only if the argument with which param
was initialized — the argument passed to logAndProcess
— was an rvalue. This is precisely what std::forward
does. That's why std::forward
is a conditional cast: it casts to an rvalue only if its argument was initialized with an rvalue.
You may wonder how std::forward
can know whether its argument was initialized with an rvalue. In the code above, for example, how can std::forward
tell whether param
was initialized with an lvalue or an rvalue? The brief answer is that that information is encoded in logAndProcess
's template parameter T
. That parameter is passed to std::forward
, which recovers the encoded information. For details on exactly how that works, consult Item 28.
Given that both std::move
and std::forward
boil down to casts, the only difference being that std::move
always casts, while std::forward
only sometimes does, you might ask whether we can dispense with std::move
and just use std::forward
everywhere. From a purely technical perspective, the answer is yes: std::forward
can do it all. std::move
isn't necessary. Of course, neither function is really necessary , because we could write casts everywhere, but I hope we agree that that would be, well, yucky.
std::move
's attractions are convenience, reduced likelihood of error, and greater clarity. Consider a class where we want to track how many times the move constructor is called. A static counter that's incremented during move construction is all we need. Assuming the only non-static data in the class is a std::string
, here's the conventional way (i.e., using std::move
) to implement the move constructor:
class Widget {
public:
Widget(Widget&& rhs)
: s( std::move(rhs.s))
{ ++moveCtorCalls; }
…
private:
static std::size_t moveCtorCalls;
std::string s;
};
To implement the same behavior with std::forward
, the code would look like this:
class Widget {
public:
Widget(Widget&& rhs) // unconventional,
: s( std::forward(rhs.s)) // undesirable
{ ++moveCtorCalls; } // implementation
…
};
Note first that std::move
requires only a function argument ( rhs.s
), while std::forward
requires both a function argument ( rhs.s
) and a template type argument ( std::string
). Then note that the type we pass to std::forward
should be a non-reference, because that's the convention for encoding that the argument being passed is an rvalue (see Item 28). Together, this means that std::move
requires less typing than std::forward
, and it spares us the trouble of passing a type argument that encodes that the argument we're passing is an rvalue. It also eliminates the possibility of our passing an incorrect type (e.g., std::string&
, which would result in the data member s
being copy constructed instead of move constructed).
More importantly, the use of std::move
conveys an unconditional cast to an rvalue, while the use of std::forward
indicates a cast to an rvalue only for references to which rvalues have been bound. Those are two very different actions. The first one typically sets up a move, while the second one just passes — forwards — an object to another function in a way that retains its original lvalueness or rvalueness. Because these actions are so different, it's good that we have two different functions (and function names) to distinguish them.
Читать дальше