There are three sets of rules for type deduction in modern C++: one for function templates, one for auto
, and one for decltype
. Without a solid understanding of how deduction operates, effective programming in modern C++ is all but impossible.
Since type deduction for templates is the basis of that for auto
, it’s important to truly understand the aspects of template type deduction that auto
builds on: during template type deduction, there are three cases for parameter types:
- pointer type or non-universal reference type
- universal reference type
- neither a pointer nor a reference (value type)
Moreover, there’s a niche case worth knowing about, that arguments that are array or function names decay to pointers unless they’re used to initialize references.
To tell the difference, let’s think of a function template as looking like this:
|
|
Case 1: ParamType
is Reference or Pointer, but not a Universal Reference
The rules in this case works like this:
- If
expr
’s type is a reference, ignore the reference part. - Then pattern-match
expr
’s type againstParamType
to determine T.
For example,
|
|
Note that even though rx’s type is a reference, T is deduced to be a non-reference, because rx’s reference-ness is ignored during type deduction.
|
|
As before, rx’s reference-ness is ignored during type deduction.
|
|
As shown above, when param were a pointer (or a pointer to const), things work essentially the same way.
Case 2: ParamType
is a Universal Reference
Things are less obvious for templates taking universal reference paramters:
- If
expr
is an lvalue, bothT
andParamType
are deduced to be lvalue references. - If
expr
is an rvalue, the “normal” (i.e., case 1) rules apply.
For example:
|
|
EMCpp Item24 explains why these examples play out the way they do.
Case 3: ParamType
is Neither a Pointer nor a Reference
In this case, we’re dealing with pass-by-value. That means that param
will be a new object, which motivates the rules below:
- As before, if
expr
’s type is a reference, ignore the reference part - If, after ignoring
expr
’s reference-ness,expr
is const, ignore that, too. If it’svolatile
, also ignore that (refer to EMCpp item 40 forvolatile
).
|
|
Array Arguments
Even though they sometimes seems to be interchangeable, array types are, in fact, different from pointer types. We had such equivalence illusion because, in many contexts, an array decays into a pointer to its first element:
|
|
Array parameter declarations are treated as if they were pointer parameters, so void myFunc(int param[]);
is equivalent to void myFunc(int* param);
. Thus, the type of an array that’s passed to a template function by value is deduced to be a pointer type:
|
|
Although functions can’t declare parameters that are truly arrays, they can declare parameters that are references to arrays. Thus,
|
|
The actual type of the array includes the array size, 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]
.
Using this ability to declare references to arrays enables creation of a template that deduces the number of elements that an array contains:
|
|
There are two points worth noting in this declaration:
-
constexpr
, as explained in EMCpp 15, makes the function result available during compilation, which makes it possible to declare an array with the same number of elements as a second array whose size is computed from a braced initializer:1 2 3
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; // 7 elements int mappedVals[arraySize(keyVals)]; // 7 elements std::array<int, arraySize(keyVals)> values; // size == 7
-
noexcept
, as explained in EMCpp 14, helps compilers generate better code.
Function Arguments
Apart from arrays, function types can decay into function pointers, too. As a result:
|
|
This rarely makes any difference in practice.