const int& rx = x; // as before
f(x); // x is lvalue, so T is int& ,
// param's type is also int&
f(cx); // cx is lvalue, so T is const int& ,
// param's type is also const int&
f(rx); // rx is lvalue, so T is const int& ,
// param's type is also const int&
f(27); // 27 is rvalue, so T is int ,
// param's type is therefore int&&
Item 24explains exactly why these examples play out the way they do. The key point here is that the type deduction rules for universal reference parameters are different from those for parameters that are lvalue references or rvalue references. In particular, when universal references are in use, type deduction distinguishes between lvalue arguments and rvalue arguments. That never happens for non-universal references.
Case 3: ParamType
is Neither a Pointer nor a Reference
When ParamType
is neither a pointer nor a reference, we're dealing with pass-by-value:
template
void f( Tparam); // param is now passed by value
That means that param
will be a copy of whatever is passed in — a completely new object. The fact that param
will be a new object motivates the rules that govern how T
is deduced from expr
:
1. As before, if expr
's type is a reference, ignore the reference part.
2. If, after ignoring expr
's reference-ness, expr
is const
, ignore that, too. If it's volatile
, also ignore that. ( volatile
objects are uncommon. They're generally used only for implementing device drivers. For details, see Item 40.)
Hence:
int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // T's and param's types are both int
f(cx); // T's and param's types are again both int
f(rx); // T's and param's types are still both int
Note that even though cx
and rx
represent const
values, param
isn't const
. That makes sense. param
is an object that's completely independent of cx
and rx
— a copy of cx
or rx.
The fact that cx
and rx
can't be modified says nothing about whether param
can be. That's why expr
's const
ness (and volatile
ness, if any) is ignored when deducing a type for param
: just because expr
can't be modified doesn't mean that a copy of it can't be.
It's important to recognize that const
(and volatile
) is ignored only for by-value parameters. As we've seen, for parameters that are references-to- or pointers-to- const
, the const
ness of expr
is preserved during type deduction. But consider the case where expr
is a const
pointer to a const
object, and expr
is passed to a by-value param
:
template
void f( Tparam); // param is still passed by value
const char* const ptr = // ptr is const pointer to const object
"Fun with pointers";
f(ptr); // pass arg of type const char * const
Here, the const
to the right of the asterisk declares ptr
to be const
: ptr
can't be made to point to a different location, nor can it be set to null. (The const
to the left of the asterisk says that what ptr
points to — the character string — is const
, hence can't be modified.) When ptr
is passed to f
, the bits making up the pointer are copied into param
. As such, the pointer itself ( ptr
) will be passed by value . In accord with the type deduction rule for by-value parameters, the const
ness of ptr
will be ignored, and the type deduced for param
will be const char*
, i.e., a modifiable pointer to a const
character string. The const
ness of what ptr
points to is preserved during type deduction, but the const
ness of ptr
itself is ignored when copying it to create the new pointer, param
.
Array Arguments
That pretty much covers it for mainstream template type deduction, but there's a niche case that's worth knowing about. It's that array types are different from pointer types, even though they sometimes seem to be interchangeable. A primary contributor to this illusion is that, in many contexts, an array decays into a pointer to its first element. This decay is what permits code like this to compile:
const char name[] = "J. P. Briggs"; // name's type is
// const char[13]
const char * ptrToName = name; // array decays to pointer
Here, the const char*
pointer ptrToName
is being initialized with name
, which is a const char[13]
. These types ( const char*
and const char[13]
) are not the same, but because of the array-to-pointer decay rule, the code compiles.
But what if an array is passed to a template taking a by-value parameter? What happens then?
template
void f(T param); // template with by-value parameter
f(name); // what types are deduced for T and param?
We begin with the observation that there is no such thing as a function parameter that's an array. Yes, yes, the syntax is legal,
void myFunc(int param []);
but the array declaration is treated as a pointer declaration, meaning that myFunc
could equivalently be declared like this:
void myFunc(int *param); // same function as above
This equivalence of array and pointer parameters is a bit of foliage springing from the C roots at the base of C++, and it fosters the illusion that array and pointer types are the same.
Because array parameter declarations are treated as if they were pointer parameters, the type of an array that's passed to a template function by value is deduced to be a pointer type. That means that in the call to the template f, its type parameter T is deduced to be const char*
:
f(name); // name is array, but T deduced as const char*
But now comes a curve ball. Although functions can't declare parameters that are truly arrays, they can declare parameters that are references to arrays! So if we modify the template f
to take its argument by reference,
template
void f(T ¶m); // template with by-reference parameter
and we pass an array to it,
f(name); // pass array to f
the type deduced for T
is the actual type of the array! That type includes the size of the array, so in this example, T
is deduced to be const char [13]
, and the type of f
's parameter (a reference to this array) is const char (&)[13]
. Yes, the syntax looks toxic, but knowing it will score you mondo points with those few souls who care.
Читать дальше