Things to Remember
• std::move
performs an unconditional cast to an rvalue. In and of itself, it doesn't move anything.
• std::forward
casts its argument to an rvalue only if that argument is bound to an rvalue.
• Neither std::move
nor std::forward
do anything at runtime.
Item 24: Distinguish universal references from rvalue references.
It's been said that the truth shall set you free, but under the right circumstances, a well-chosen lie can be equally liberating. This Item is such a lie. Because we're dealing with software, however, let's eschew the word “lie” and instead say that this Item comprises an “abstraction.”
To declare an rvalue reference to some type T
, you write T&&
. It thus seems reasonable to assume that if you see “ T&&
” in source code, you're looking at an rvalue reference. Alas, it's not quite that simple:
void f(Widget &¶m); // rvalue reference
Widget &&var1 = Widget(); // rvalue reference
auto &&var2 = var1; // not rvalue reference
template
void f(std::vector &¶m); // rvalue reference
template
void f(T &¶m); // not rvalue reference
In fact, “ T&&
” has two different meanings. One is rvalue reference, of course. Such references behave exactly the way you expect: they bind only to rvalues, and their primary raison d'être is to identify objects that may be moved from.
The other meaning for “ T&&
” is either rvalue reference or lvalue reference. Such references look like rvalue references in the source code (i.e., “ T&&
”), but they can behave as if they were lvalue references (i.e., “ T&
”). Their dual nature permits them to bind to rvalues (like rvalue references) as well as lvalues (like lvalue references). Furthermore, they can bind to const
or non- const
objects, to volatile
or non- volatile
objects, even to objects that are both const
and volatile
. They can bind to virtually anything . Such unprecedentedly flexible references deserve a name of their own. I call them universal references . [11] Item 25 explains that universal references should almost always have std::forward applied to them, and as this book goes to press, some members of the C++ community have started referring to universal references as forwarding references .
Universal references arise in two contexts. The most common is function template parameters, such as this example from the sample code above:
template
void f( T&¶m); // param is a universal reference
The second context is auto
declarations, including this one from the sample code above:
auto &&var2 = var1; // var2 is a universal reference
What these contexts have in common is the presence of type deduction . In the template f
, the type of param
is being deduced, and in the declaration for var2
, var2
's type is being deduced. Compare that with the following examples (also from the sample code above), where type deduction is missing. If you see “ T&&
” without type deduction, you're looking at an rvalue reference:
void f(Widget &¶m); // no type deduction;
// param is an rvalue reference
Widget &&var1 = Widget(); // no type deduction;
// var1 is an rvalue reference
Because universal references are references, they must be initialized. The initializer for a universal reference determines whether it represents an rvalue reference or an lvalue reference. If the initializer is an rvalue, the universal reference corresponds to an rvalue reference. If the initializer is an lvalue, the universal reference corresponds to an lvalue reference. For universal references that are function parameters, the initializer is provided at the call site:
template
void f(T&& param); // param is a universal reference
Widget w;
f( w); // lvalue passed to f; param's type is
// Widget& (i.e., an lvalue reference)
f( std::move(w)); // rvalue passed to f; param's type is
// Widget&& (i.e., an rvalue reference)
For a reference to be universal, type deduction is necessary, but it's not sufficient. The form of the reference declaration must also be correct, and that form is quite constrained. It must be precisely “ T&&
”. Look again at this example from the sample code we saw earlier:
template
void f( std::vector&¶m); // param is an rvalue reference
When f
is invoked, the type T
will be deduced (unless the caller explicitly specifies it, an edge case we'll not concern ourselves with). But the form of param
's type declaration isn't “ T&&
”, it's “ std::vector&&
”. That rules out the possibility that param
is a universal reference. param
is therefore an rvalue reference, something that your compilers will be happy to confirm for you if you try to pass an lvalue to f
:
std::vector v;
f(v); // error! can't bind lvalue to
// rvalue reference
Even the simple presence of a const
qualifier is enough to disqualify a reference from being universal:
template
void f( constT&& param); // param is an rvalue reference
If you're in a template and you see a function parameter of type “ T&&
”, you might think you can assume that it's a universal reference. You can't. That's because being in a template doesn't guarantee the presence of type deduction. Consider this push_back
member function in std::vector
:
template> // from C++
class vector { // Standards
public:
void push_back( T&&x);
…
};
push_back
's parameter certainly has the right form for a universal reference, but there's no type deduction in this case. That's because push_back
can't exist without a particular vector
instantiation for it to be part of, and the type of that instantiation fully determines the declaration for push_back
. That is, saying
std::vector v;
causes the std::vector template to be instantiated as follows:
class vector> {
public:
void push_back( Widget&&x); // rvalue reference
…
};
Now you can see clearly that push_back
employs no type deduction. This push_back
for vector
(there are two — the function is overloaded) always declares a parameter of type rvalue-reference-to- T
.
In contrast, the conceptually similar emplace_back
member function in std::vector
does employ type deduction:
template> // still from
class vector { // C++
public: // Standards
Читать дальше