1. Deducing Types¶
Item 1: Understand template type deduction¶
Key idea:
If the function template looks like this:
template <typename T> void f(ParamType param);then two types are deduced: one for
T
and one forParamType
. These types are often different, becauseParamType
can contain adornments, e.g. const or reference qualifiers.
template <typename T>
void f(const T& param) {} // ParamType is const T&
int main() {
int x = 0;
f(x); // call f with an int
}
Case 1: ParamType is a Reference or Pointer, but not a Universal Reference¶
Key idea:
Considering the general form for templates and calls to it:
template <typename T> void f(ParamType param); f(expr); // deduce T and ParamType from exprthen, in the simplest case when
ParamType
is a reference type or a pointer type, but not a universal reference, type deduction works like this:
- If expr’s type is a reference, ignore the reference part.
- Then pattern-match expr’s type against
ParamType
to determineT
.
template <typename T>
void f(T& param) {} // param is a reference
int main() {
int x = 27; // x is an int
const int cx = x; // cx is a const int
const int& rx = x; // rx is a reference to x as a const int
f(x); // T is int, param's type is int&
f(cx); // T is const int,
// param's type is const int&
f(rx); // T is const int,
// param's type is const int&
}
Key idea:
Considering the general form for templates and calls to it:
template <typename T> void f(ParamType param); f(expr); // deduce T and ParamType from exprthen, in the simplest case when
ParamType
is a pointer type or a reference type, but not a universal reference, type deduction works like this:
- If expr’s type is a reference, ignore the reference part.
- Then pattern-match expr’s type against
ParamType
to determineT
.If the type of f’s parameter is changed from
T&
toconst T&
, the constness of cx and rx continues to be respected, but because we’re now assuming that param is a reference-to-const, there’s no longer a need for const to be deduced as part ofT
.
template <typename T>
void f(const T& param) {} // param is now a ref-to-const
int main() {
int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // T is int, param's type is const int&
f(cx); // T is int, param's type is const int&
f(rx); // T is int, param's type is const int&
}
Key idea:
Considering the general form for templates and calls to it:
template <typename T> void f(ParamType param); f(expr); // deduce T and ParamType from exprthen, in the simplest case when
ParamType
is a pointer type or a reference type, but not a universal reference, type deduction works like this:
- If expr’s type is a reference, ignore the reference part.
- Pattern-match expr’s type against
ParamType
to determineT
.If param were a pointer (or a pointer to const) instead of a reference, things would work essentially the same way.
template <typename T>
void f(T* param) {} // param is now a pointer
int main() {
int x = 27; // as before
const int* px = &x; // px is a ptr to x as a const int
f(&x); // T is int, param's type is int*
f(px); // T is const int,
// param's type is const int*
}
Case 2: ParamType is a Universal Reference¶
Key idea:
Considering the general form for templates and calls to it:
template <typename T> void f(ParamType param); f(expr); // deduce T and ParamType from exprthen, in the case when
ParamType
is a universal reference type, type deduction works like this:
- If expr is an lvalue, both
T
andParamType
are deduced to be lvalue references- If expr is an rvalue, the usual type deduction rules apply.
template <typename T>
void f(T&& param) {} // param is now a universal reference
int main() {
int x = 27; // as before
const int cx = x; // as before
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&&
}
Case 3: ParamType is Neither a Pointer nor a Reference¶
Key idea:
If we’re dealing with pass-by-value
template <typename T> void f(T param); // param is now passed by valueThat 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:
- 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’s volatile, also ignore that. (volatile objects are uncommon. They’re generally used only for implementing device drivers.)
template <typename T>
void f(T param) {} // param is now passed by value
int main() {
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
const char* const ptr = // ptr is const pointer to const object
"Fun with pointers";
f(ptr); // pass arg of type const char * const
}
Array Arguments¶
Key idea:
In many contexts, an array decays into a pointer to its first element.
int main() {
const char name[] = "J. P. Briggs"; // name's type is
// const char[13]
const char* ptrToName = name; // array decays to pointer
}
Key idea:
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.
template <typename T>
void f(T param) {} // template with by-value parameter
int main() {
const char name[] = "J. P. Briggs"; // name's type is
// const char[13]
f(name); // what types are deduced for T and param?
// -> name is array, but T deduced as const char*
}
Key idea:
Although functions can’t declare parameters that are truly arrays, they can declare parameters that are references to arrays.
The type deduced for
T
is the actual type of the array! That type includes the size of the array, so in this exampleT
is deduced to beconst char[13]
, and the type of f’s parameter (a reference to this array) isconst char (&)[13]
.
template <typename T>
void f(T& param) {} // template with by-reference parameter
int main() {
const char name[] = "J. P. Briggs"; // name's type is
// const char[13]
f(name); // pass array to f
}
Key idea:
The ability to declare references to arrays enables creation of a template to deduce the number of elements that an array contains.
#include <array>
#include <cstddef>
// return size of an array as a compile-time constant. (The
// array parameter has no name, because we care only about
// the number of elements it contains.)
template <typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept {
return N;
}
// keyVals has 7 elements
int keyVals[] = {1, 3, 7, 9, 11, 22, 35};
// so does mappedVals
int mappedVals1[arraySize(keyVals)];
// mappedVals' size is 7
std::array<int, arraySize(keyVals)> mappedVals2;
Function Arguments¶
Key-idea:
Function types can decay into pointers, too, and everything regarding type deduction and arrays applies to type deduction for functions and their decay into function pointers.
void someFunc(int, double) {} // someFunc is a function;
// type is void(int, double)
template <typename T>
void f1(T param) {} // in f1, param passed by value
template <typename T>
void f2(T& param) {} // in f2, param passed by ref
int main() {
f1(someFunc); // param deduced as ptr-to-func;
// type is void (*)(int, double)
f2(someFunc); // param deduced as ref-to-func;
// type is void (&)(int, double)
}
Identical function declarations.
void myFunc1(int param[]) {}
void myFunc2(int* param) {} // same function as above
Things to Remember¶
- During template type deduction, arguments that are references are treated as non-references, i.e. their reference-ness is ignored.
- When deducing types for universal reference parameters, lvalue arguments get special treatment and are deduced as lvalue references. It’s the only situation in template type deduction where T is deduced to be a reference
- When deducing types for by-value parameters, const or volatile arguments are treated as non-const and non-volatile.
- During template type deduction, arguments that are array or function names decay to pointers, unless they’re used to initialize references.
Item 2: Understand auto type deduction¶
Key idea:
Deducing types for auto is the same as deducing types for templates (with only one curious exception).
template <typename T> // conceptual template for
void func_for_x(T param) {} // deducing x's type
template <typename T> // conceptual template for
void func_for_cx(const T param) {} // deducing cx's type
template <typename T> // conceptual template for
void func_for_rx(const T& param) {} // deducing rx's type
void someFunc(int, double) {} // someFunc is a function;
// type is void(int, double)
int main() {
auto x = 27; // case 3 (x is neither ptr nor reference)
const auto cx = x; // case 3 (cx isn't either)
const auto& rx = x; // case 1 (rx is a non-universal ref.)
auto&& uref1 = x; // x is int and lvalue,
// so uref1's type is int&
auto&& uref2 = cx; // cx is const int and lvalue
// so uref2's type is const int&
auto&& uref3 = 27; // 27 is int and rvalue,
// so uref3's type is int&&
func_for_x(27); // conceptual call: param's
// deduced type is x's type
func_for_cx(x); // conceptual call: param's
// deduced type is cx's type
func_for_rx(x); // conceptual call: param's
// deduced type is rx's type
const char name[] = // name's type is const char[13]
"R. N. Briggs";
auto arr1 = name; // arr1's type is const char*
auto& arr2 = name; // arr2's type is
// const char (&)[13]
auto func1 = someFunc; // func1's type is
// void (*)(int, double)
auto& func2 = someFunc; // func2's type is
// void (&)(int, double)
}
Key idea:
The treatment of braced initializers is the only way in which auto type deduction and template type deduction differ.
#include <initializer_list>
template <typename T> // template with parameter
void f(T param) {} // declaration equivalent to
// x's declaration
template <typename T>
void f2(std::initializer_list<T> initList) {}
int main() {
{
int x1 = 27;
int x2(27);
int x3 = {27};
int x4{27};
}
{
auto x1 = 27; // type is int, value is 27
auto x2(27); // ditto
auto x3 = {27}; // type is std::initializer_list<int>,
// value is {27}
auto x4{27}; // ditto
// Error! Can't deduce T for std::initializer_list<T>
// auto x5 = {1, 2, 3.0};
}
{
// x's type is std::initializer_list<int>
auto x = {11, 23, 9};
// Error! Can't deduce type for T
// f({ 11, 23, 9 });
// T deduced as int, and initList's type is std::initializer_list<int>
f2({11, 23, 9});
}
}
Key ideas:
- A function with an auto return type that returns a braced initializer list won’t compile.
- When auto is used in a parameter type specification in a C++14 lambda expression, things won’t compile.
#include <vector>
auto createInitList() {
// return {1, 2, 3}; // error: can't deduce type
// for {1, 2, 3}
}
int main() {
std::vector<int> v;
auto resetV = [&v](const auto& newValue) { v = newValue; }; // C++14
// Error! Can't deduce type for { 1, 2, 3 }
// resetV( {1, 2, 3} );
}
Things to Remember¶
- auto type deduction is usually the same as template type deduction, but auto
type deduction assumes that a braced initializer represents a
std::initializer_list
, and template type deduction doesn’t. - auto in a function return type or a lambda parameter implies template type deduction, not auto type deduction.