Consider the effect that has when compilers have to determine which std::stringconstructor 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 Annotationconstructor'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- constis permitted to bind to a constrvalue. The member initialization therefore invokes the copy constructor in std::string, even though texthas 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 constobjects 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 constif you want to be able to move from them. Move requests on constobjects are silently transformed into copy operations. Second, std::movenot 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::moveto an object is that it's an rvalue.
The story for std::forwardis similar to that for std::move, but whereas std::move unconditionally casts its argument to an rvalue, std::forwarddoes it only under certain conditions. std::forwardis a conditional cast. To understand when it casts and when it doesn't, recall how std::forwardis 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 paramis passed to the function process. processis overloaded for lvalues and rvalues. When we call logAndProcesswith an lvalue, we naturally expect that lvalue to be forwarded to processas an lvalue, and when we call logAndProcesswith an rvalue, we expect the rvalue overload of processto be invoked.
But param, like all function parameters, is an lvalue. Every call to processinside logAndProcesswill thus want to invoke the lvalue overload for process. To prevent this, we need a mechanism for paramto be cast to an rvalue if and only if the argument with which paramwas initialized — the argument passed to logAndProcess— was an rvalue. This is precisely what std::forwarddoes. That's why std::forwardis a conditional cast: it casts to an rvalue only if its argument was initialized with an rvalue.
You may wonder how std::forwardcan know whether its argument was initialized with an rvalue. In the code above, for example, how can std::forwardtell whether paramwas 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::moveand std::forwardboil down to casts, the only difference being that std::movealways casts, while std::forwardonly sometimes does, you might ask whether we can dispense with std::moveand just use std::forwardeverywhere. From a purely technical perspective, the answer is yes: std::forwardcan do it all. std::moveisn'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::moverequires only a function argument ( rhs.s), while std::forwardrequires both a function argument ( rhs.s) and a template type argument ( std::string). Then note that the type we pass to std::forwardshould 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::moverequires 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 sbeing copy constructed instead of move constructed).
More importantly, the use of std::moveconveys an unconditional cast to an rvalue, while the use of std::forwardindicates 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.
Читать дальше