}
This is the form you'll see in, among other places, the standard containers' emplacement functions (see Item 42) and the smart pointer factory functions, std::make_shared
and std::make_unique
(see Item 21).
Given our target function f
and our forwarding function fwd
, perfect forwarding fails if calling f
with a particular argument does one thing, but calling fwd
with the same argument does something different:
f( expression ); // if this does one thing,
fwd( expression ); // but this does something else, fwd fails
// to perfectly forward expression to f
Several kinds of arguments lead to this kind of failure. Knowing what they are and how to work around them is important, so let's tour the kinds of arguments that can't be perfect-forwarded.
Braced initializers
Suppose f is declared like this:
void f(const std::vector& v);
In that case, calling f
with a braced initializer compiles,
f( { 1, 2, 3 }); // fine, "{1, 2, 3}" implicitly
// converted to std::vector
but passing the same braced initializer to fwd
doesn't compile:
fwd( { 1, 2, 3 }); // error! doesn't compile
That's because the use of a braced initializer is a perfect forwarding failure case.
All such failure cases have the same cause. In a direct call to f
(such as f({ l, 2, 3 })
), compilers see the arguments passed at the call site, and they see the types of the parameters declared by f
. They compare the arguments at the call site to the parameter declarations to see if they're compatible, and, if necessary, they perform implicit conversions to make the call succeed. In the example above, they generate a temporary std::vector
object from { 1, 2, 3 }
so that f
's parameter v
has a std::vector
object to bind to.
When calling f
indirectly through the forwarding function template fwd
, compilers no longer compare the arguments passed at fwd
's call site to the parameter declarations in f
. Instead, they deduce the types of the arguments being passed to fwd
, and they compare the deduced types to f
's parameter declarations. Perfect forwarding fails when either of the following occurs:
• Compilers are unable to deduce a typefor one or more of fwd
's parameters. In this case, the code fails to compile.
• Compilers deduce the “wrong” typefor one or more of fwd
's parameters. Here, “wrong” could mean that fwd
's instantiation won't compile with the types that were deduced, but it could also mean that the call to f
using fwd
's deduced types behaves differently from a direct call to f
with the arguments that were passed to fwd
. One source of such divergent behavior would be if f
were an overloaded function name, and, due to “incorrect” type deduction, the overload of f
called inside fwd
were different from the overload that would be invoked if f
were called directly.
In the “ fwd({ 1, 2, 3 })
” call above, the problem is that passing a braced initializer to a function template parameter that's not declared to be a std::initializer_list
is decreed to be, as the Standard puts it, a “non-deduced context.” In plain English, that means that compilers are forbidden from deducing a type for the expression { 1, 2, 3 }
in the call to fwd
, because fwd
's parameter isn't declared to be a std::initializer_list
. Being prevented from deducing a type for fwd
's parameter, compilers must understandably reject the call.
Interestingly, Item 2explains that type deduction succeeds for auto
variables initialized with a braced initializer. Such variables are deemed to be std::initializer_list
objects, and this affords a simple workaround for cases where the type the forwarding function should deduce is a std::initializer_list
— declare a local variable using auto
, then pass the local variable to the forwarding function:
auto il = { 1, 2, 3 }; // il's type deduced to be
// std::initializer_list
fwd(il); // fine, perfect-forwards il to f
0
or NULL
as null pointers
Item 8explains that when you try to pass 0
or NULL
as a null pointer to a template, type deduction goes awry, deducing an integral type (typically int
) instead of a pointer type for the argument you pass. The result is that neither 0
nor NULL
can be perfect-forwarded as a null pointer. The fix is easy, however: pass nullptr
instead of 0
or NULL
. For details, consult Item 8.
Declaration-only integral static const
data members
As a general rule, there's no need to define integral static const
data members in classes; declarations alone suffice. That's because compilers perform const propagation on such members' values, thus eliminating the need to set aside memory for them. For example, consider this code:
class Widget {
public:
static const std::size_t MinVals = 28;// MinVals' declaration
…
};
… // no defn. for MinVals
std::vector widgetData;
widgetData.reserve( Widget::MinVals); // use of MinVals
Here, we're using Widget::MinVals
(henceforth simply MinVals
) to specify widgetData
's initial capacity, even though MinVals
lacks a definition. Compilers work around the missing definition (as they are required to do) by plopping the value 28 into all places where MinVals
is mentioned. The fact that no storage has been set aside for MinVals
' value is unproblematic. If MinVals
' address were to be taken (e.g., if somebody created a pointer to MinVals
), then MinVals
would require storage (so that the pointer had something to point to), and the code above, though it would compile, would fail at link-time until a definition for MinVals
was provided.
With that in mind, imagine that f
(the function fwd
forwards its argument to) is declared like this:
void f(std::size_t val);
Calling f
with MinVals
is fine, because compilers will just replace MinVals
with its value:
f( Widget::MinVals); // fine, treated as "f(28)"
Alas, things may not go so smoothly if we try to call f
through fwd
:
fwd( Widget::MinVals); // error! shouldn't link
This code will compile, but it shouldn't link. If that reminds you of what happens if we write code that takes MinVals
' address, that's good, because the underlying problem is the same.
Although nothing in the source code takes MinVals
' address, fwd
's parameter is a universal reference, and references, in the code generated by compilers, are usually treated like pointers. In the program's underlying binary code (and on the hardware), pointers and references are essentially the same thing. At this level, there's truth to the adage that references are simply pointers that are automatically dereferenced. That being the case, passing MinVals
by reference is effectively the same as passing it by pointer, and as such, there has to be some memory for the pointer to point to. Passing integral static const
data members by reference, then, generally requires that they be defined, and that requirement can cause code using perfect forwarding to fail where the equivalent code without perfect forwarding succeeds.
Читать дальше