But perhaps you noticed the weasel words I sprinkled through the preceding discussion. The code “shouldn't” link. References are “usually” treated like pointers. Passing integral static const
data members by reference “generally” requires that they be defined. It's almost like I know something I don't really want to tell you.
That's because I do. According to the Standard, passing MinVals
by reference requires that it be defined. But not all implementations enforce this requirement. So, depending on your compilers and linkers, you may find that you can perfect-forward integral static const
data members that haven't been defined. If you do, congratulations, but there is no reason to expect such code to port. To make it portable, simply provide a definition for the integral static const
data member in question. For MinVals
, that'd look like this:
const std::size_t Widget::MinVals; // in Widget's .cpp file
Note that the definition doesn't repeat the initializer (28, in the case of MinVals
). Don't stress over this detail, however. If you forget and provide the initializer in both places, your compilers will complain, thus reminding you to specify it only once.
Overloaded function names and template names
Suppose our function f (the one we keep wanting to forward arguments to via fwd) can have its behavior customized by passing it a function that does some of its work. Assuming this function takes and returns int
s, f
could be declared like this:
void f( int (*pf)(int)); // pf = "processing function"
It's worth noting that f
could also be declared using a simpler non-pointer syntax. Such a declaration would look like this, though it'd have the same meaning as the declaration above:
void f( int pf(int)); // declares same f as above
Either way, now suppose we have an overloaded function, processVal
:
int processVal(int value);
int processVal(int value, int priority);
We can pass processVal
to f
,
f(processVal); // fine
but it's something of a surprise that we can. f
demands a pointer to a function as its argument, but processVal
isn't a function pointer or even a function, it's the name of two different functions. However, compilers know which processVal
they need: the one matching f
's parameter type. They thus choose the processVal
taking one int
, and they pass that function's address to f
.
What makes this work is that f
's declaration lets compilers figure out which version of processVal
is required. fwd
, however, being a function template, doesn't have any information about what type it needs, and that makes it impossible for compilers to determine which overload should be passed:
fwd(processVal); // error! which processVal?
processVal
alone has no type. Without a type, there can be no type deduction, and without type deduction, we're left with another perfect forwarding failure case.
The same problem arises if we try to use a function template instead of (or in addition to) an overloaded function name. A function template doesn't represent one function, it represents many functions:
template
T workOnVal(T param) // template for processing values
{ … }
fwd(workOnVal); // error! which workOnVal
// instantiation?
The way to get a perfect-forwarding function like fwd
to accept an overloaded function name or a template name is to manually specify the overload or instantiation you want to have forwarded. For example, you can create a function pointer of the same type as f
's parameter, initialize that pointer with processVal
or workOnVal
(thus causing the proper version of processVal
to be selected or the proper instantiation of workOnVal
to be generated), and pass the pointer to fwd
:
using ProcessFuncType = // make typedef;
int (*)(int); // see Item 9
ProcessFuncTypeprocessValPtr = processVal; // specify needed
// signature for
// processVal
fwd( processValPtr); // fine
fwd( static_cast(workOnVal)); // also fine
Of course, this requires that you know the type of function pointer that fwd
is forwarding to. It's not unreasonable to assume that a perfect-forwarding function will document that. After all, perfect-forwarding functions are designed to accept anything , so if there's no documentation telling you what to pass, how would you know?
Bitfields
The final failure case for perfect forwarding is when a bitfield is used as a function argument. To see what this means in practice, observe that an IPv4 header can be modeled as follows: [13] This assumes that bitfields are laid out lsb (least significant bit) to msb (most significant bit). C++ doesn't guarantee that, but compilers often provide a mechanism that allows programmers to control bitfield layout.
struct IPv4Header {
std::uint32_t version:4,
IHL:4,
DSCP:6,
ECN:2,
totalLength:16;
…
};
If our long-suffering function f
(the perennial target of our forwarding function fwd
) is declared to take a std::size_t
parameter, calling it with, say, the totalLength
field of an IPv4Header
object compiles without fuss:
void f(std::size_t sz); // function to call
IPv4Header h;
…
f(h.totalLength); // fine
Trying to forward h.totalLength
to f
via fwd
, however, is a different story:
fwd(h.totalLength); // error!
The problem is that fwd
's parameter is a reference, and h.totalLength
is a non- const
bitfield. That may not sound so bad, but the C++ Standard condemns the combination in unusually clear prose: “A non- const
reference shall not be bound to a bit-field.” There's an excellent reason for the prohibition. Bitfields may consist of arbitrary parts of machine words (e.g., bits 3-5 of a 32-bit int
), but there's no way to directly address such things. I mentioned earlier that references and pointers are the same thing at the hardware level, and just as there's no way to create a pointer to arbitrary bits (C++ dictates that the smallest thing you can point to is a char
), there's no way to bind a reference to arbitrary bits, either.
Working around the impossibility of perfect-forwarding a bitfield is easy, once you realize that any function that accepts a bitfield as an argument will receive a copy of the bitfield's value. After all, no function can bind a reference to a bitfield, nor can any function accept pointers to bitfields, because pointers to bitfields don't exist. The only kinds of parameters to which a bitfield can be passed are by-value parameters and, interestingly, references-to- const
. In the case of by-value parameters, the called function obviously receives a copy of the value in the bitfield, and it turns out that in the case of a reference-to- const
parameter, the Standard requires that the reference actually bind to a copy of the bitfield's value that's stored in an object of some standard integral type (e.g., int
). References-to- const
don't bind to bitfields, they bind to “normal” objects into which the values of the bitfields have been copied.
Читать дальше