void func( Widget¶m);
The answer is reference collapsing . Yes, you are forbidden from declaring references to references, but compilers may produce them in particular contexts, template instantiation being among them. When compilers generate references to references, reference collapsing dictates what happens next.
There are two kinds of references (lvalue and rvalue), so there are four possible reference-reference combinations (lvalue to lvalue, lvalue to rvalue, rvalue to lvalue, and rvalue to rvalue). If a reference to a reference arises in a context where this is permitted (e.g., during template instantiation), the references collapse to a single reference according to this rule:
If either reference is an lvalue reference, the result is an lvalue reference. Otherwise (i.e., if both are rvalue references) the result is an rvalue reference.
In our example above, substitution of the deduced type Widget&
into the template func
yields an rvalue reference to an lvalue reference, and the reference-collapsing rule tells us that the result is an lvalue reference.
Reference collapsing is a key part of what makes std::forward
work. As explained in Item 25, std::forward
is applied to universal reference parameters, so a common use case looks like this:
template
void f(T&& fParam)
{
… // do some work
someFunc( std::forward(fParam)); // forward fParam to
} // someFunc
Because fParam
is a universal reference, we know that the type parameter T
will encode whether the argument passed to f
(i.e., the expression used to initialize fParam
) was an lvalue or an rvalue. std::forward
's job is to cast fParam
(an lvalue) to an rvalue if and only if T
encodes that the argument passed to f
was an rvalue, i.e., if T
is a non-reference type.
Here's how std::forward
can be implemented to do that:
template // in
TSS forward(typename // namespace
remove_reference::type& param) // std
{
return static_cast(param);
}
This isn't quite Standards-conformant (I've omitted a few interface details), but the differences are irrelevant for the purpose of understanding how std::forward
behaves.
Suppose that the argument passed to f is an lvalue of type Widget
. T
will be deduced as Widget&
, and the call to std::forward
will instantiate as std::forward
. Plugging Widget&
into the std::forward
implementation yields this:
Widget&&& forward(typename
remove_reference< Widget&>::type& param)
{ return static_cast< Widget&&&>(param); }
The type trait std::remove_reference::type
yields Widget
(see Item 9), so std::forward
becomes:
Widget& && forward( Widget& param)
{ return static_cast(param); }
Reference collapsing is also applied to the return type and the cast, and the result is the final version of std::forward
for the call:
Widget&forward( Widget¶m) // still in
{ return static_cast< Widget&>(param); } // namespace std
As you can see, when an lvalue argument is passed to the function template f
, std::forward
is instantiated to take and return an lvalue reference. The cast inside std::forward
does nothing, because param
's type is already Widget&
, so casting it to Widget&
has no effect. An lvalue argument passed to std::forward
will thus return an lvalue reference. By definition, lvalue references are lvalues, so passing an lvalue to std::forward
causes an lvalue to be returned, just like it's supposed to.
Now suppose that the argument passed to f
is an rvalue of type Widget
. In this case, the deduced type for f
's type parameter T
will simply be Widget
. The call inside f
to std::forward
will thus be to std::forward
. Substituting Widget
for T
in the std::forward
implementation gives this:
Widget&& forward(typename
remove_reference< Widget>::type& param)
{ return static_cast< Widget&&>(param); }
Applying std::remove_reference
to the non-reference type Widget
yields the same type it started with ( Widget
), so std::forward
becomes this:
Widget&& forward( Widget& param)
{ return static_cast(param); }
There are no references to references here, so there's no reference collapsing, and this is the final instantiated version of std::forward
for the call.
Rvalue references returned from functions are defined to be rvalues, so in this case, std::forward
will turn f
's parameter fParam
(an lvalue) into an rvalue. The end result is that an rvalue argument passed to f
will be forwarded to someFunc
as an rvalue, which is precisely what is supposed to happen.
In C++14, the existence of std::remove_reference_t
makes it possible to implement std::forward
a bit more concisely:
template // C++14; still in
T&& forward( remove_reference_t& param) // namespace std
{
return static_cast(param);
}
Reference collapsing occurs in four contexts. The first and most common is template instantiation. The second is type generation for auto
variables. The details are essentially the same as for templates, because type deduction for auto
variables is essentially the same as type deduction for templates (see Item 2). Consider again this example from earlier in the Item:
template
void func(T&& param);
Widget widgetFactory(); // function returning rvalue
Widget w; // a variable (an lvalue)
func(w); // call func with lvalue; T deduced
// to be Widget&
func(widgetFactory()); // call func with rvalue; T deduced
// to be Widget
This can be mimicked in auto
form. The declaration
auto&&w1 = w;
initializes w1
with an lvalue, thus deducing the type Widget&
for auto
. Plugging Widget&
in for auto
in the declaration for w1
yields this reference-to-reference code,
Widget& &&w1 = w;
which, after reference collapsing, becomes
Widget&w1 = w;
As a result, w1
is an lvalue reference.
On the other hand, this declaration,
auto&&w2 = widgetFactory();
initializes w2
with an rvalue, causing the non-reference type Widget
to be deduced for auto
. Substituting Widget
for auto
gives us this:
Читать дальше