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
Tand one forParamType. These types are often different, becauseParamTypecan 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
ParamTypeis 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
ParamTypeto 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
ParamTypeis 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
ParamTypeto 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
ParamTypeis 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
ParamTypeto 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
ParamTypeis a universal reference type, type deduction works like this:
- If expr is an lvalue, both
TandParamTypeare 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
Tis 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
Tis the actual type of the array! That type includes the size of the array, so in this exampleTis 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.