Template arguments are deduced for our convenience, so that we do not have to provide them explicitly (and possibly make a mistake). A compiler deduces template arguments in the following cases:
a call to a non-member template function (the most frequent),
a call to a member template function (including a constructor),
an initialization of a variable of type auto (including a function
parameter and a return value).
We discuss the deduction using a non-member function template. To call a template function, a compiler has to instantiate a function template, i.e., produce the code of a template function based on the function template and its arguments.
A compiler deduces template arguments (of a function) based on:
types of function parameters,
types and categories of function arguments.
How an argument category affects deduction is quite complex, and therefore is described in a separate topic on perfect argument forwarding.
In the simplest case we call a function with a single parameter:
template <parameter list>
void
foo(ParameterType t)
{
// Body of a function template.
}
int
main()
{
foo(expr);
}
A compiler is supposed to deduce arguments for parameters (defined in
parameter list) of function template foo based on expression
expr and type ParameterType of parameter t of function foo.
To talk about deduction, type ParameterType must depend on (use in
its definition) at least one template parameter. There are many ways
type ParameterType can depend on template parameters, and we discuss
the most important.
Rule: the deduced argument should allow for the initialization of the function parameter.
A parameter is always initialized with an argument, either explicit or default.
No conversion: initialization without type conversion.
We mean the conversion from the type of function argument expr to
type ParameterType of a function parameter. Let’s note that such
type conversions are allowed for regular (non-template) functions.
The initialization of parameter t in the above simplest case looks
like this:
ParameterType t = expr;
A compiler must deduce arguments for template parameters used in the
definition of type ParameterType, so that the initialization of a
function parameter is possible without type conversion. Deduction may
turn out impossible, making instantiation impossible.
If ParameterType is a reference type to a const value of type T,
where T is a template parameter, and the function argument is 1,
then the initialization looks like this:
const T &t = 1;
The deduced type is T = int, because the initialization is possible
without type conversion.
However, if ParameterType is a reference type to a non-const value
of type T, then the initialization looks like this:
T &t = 1;
The deduced type is still T = int, because an rvalue of a
fundamental type (literal 1) is of a non-const type (so the standard
says). Therefere instantiation cannot succeed, because non-const
lvalue reference t cannot be initialized with an rvalue.
There is something wrong with the “no conversion” in the above
examples: ParameterType is const int & (or int &), while
expression 1 is of type int! Aren’t they supposed to be the
same? No: function parameter type ParameterType and the type of
argument expr can differ with the top-level qualifiers and
declarator &, which follows from how variables can be initialized,
as discussed below.
The place of a qualifier or a declarator in a defined type affects:
initialization of a value of this type,
function overloading depending on the parameter of this type.
Type qualifiers and declarators can be top-level.
Declarators * of a pointer type and & of a reference type can be
used in various places in a defined type. A top-level qualifier
is the first one from the right. For instance, for type int * const
& the top-level qualifier is &.
Type qualifiers (const and volatile) can be top-level for any
type except a reference type.
The defined type (non-pointer, non-reference) can have a qualifier
given either before or after the used type. There are no other places
for a qualifier, and the place makes no difference. Qualifiers of a
regular type are called top-level, even though they can’t be
lower-level. For instance, const int and int const define the
same type, and const we call a top-level qualifier.
Those qualifiers matter only during compilation (not at run time): a
compiler cannot allow a data of a const type to be modified, and
should not optimize the access to the data of a volatile type.
Leeway. We can initialize a variable with an initializing expression, even if their (regular) types differ with (top-level) qualifiers, because it’s about copying a value. For example:
#include <concepts>
int main()
{
static_assert(std::same_as<const int, int const>);
int a1 = 1;
const int a2 = 2;
volatile int a3 = 3;
const volatile int a4 = 4;
int b1 = a4;
const int b2 = a3;
volatile int b3 = a2;
const volatile int b4 = a1;
}
This leeway applies to the initialization of a function parameter with a function argument, which yields the following limitation.
Limitation. We cannot overload a function depending on the qualifiers of a regular type (of a function parameter), because the initialization of a parameter of a regular type (where the leeway puts no requirements on the qualifiers) is unable to affect overload resolution.
These qualifiers (that are an implementation detail of a function
body) do not matter for a caller and so they are removed by a compiler
from a function signature (as they are not part of a function
interface) to enable linking. Please check (using nm) the function
signatures in the symbol table of the following program.
void foo(int)
{
}
// This function has same signature as the one above even though their
// parameters differ with the qualifiers.
// void foo(const int)
// {
// }
int main()
{
const int i = 1;
foo(i);
// Can point to a function with a regular parameter that is either
// non-const or const.
void (*fp1)(int) = foo;
void (*fp2)(const int) = foo;
}
The top-level qualifiers of a pointer type are located on the right of
the top-level * declarator, i.e., at the end of the type definition
(or at the beginning, reading from the right as we should). They
qualify a type of a pointer variable, not the type of the data pointed
to.
Just like a variable of a regular type, we can initialize a variable of a pointer type using a value whose (pointer) type differs with the top-level qualifiers, because the value is copied. And therefore we cannot overload a function depending on the pointer types (of a function parameter) that differ with the top-level qualifiers. Here’s an example:
void foo(int *)
{
}
// This function has same signature as the one above.
// void foo(int * const)
// {
// }
int main()
{
int i = 1;
int * const p1 = &i;
int * p2 = p1;
foo(p1);
foo(p2);
}
In a pointer type, on the left of the * top-level declarator, we can
put the top-level qualifiers for the type pointed to. We can call
these qualifiers lower-level, if we talk about the pointer type.
A requirement for a pointer type. The lower-level qualifiers of a pointer type must include the top-level qualifiers of the type pointed to.
Here’s an example:
int main()
{
int a1 = 1;
const int a2 = 2;
volatile int a3 = 3;
const volatile int a4 = 4;
// The commented lines below would bypass the qualifiers.
int * p11 = &a1;
// int * p12 = &a2;
// int * p13 = &a3;
// int * p14 = &a4;
const int * p21 = &a1;
const int * p22 = &a2;
// const int * p23 = &a3;
// const int * p24 = &a4;
volatile int * p31 = &a1;
// volatile int * p32 = &a2;
volatile int * p33 = &a3;
// volatile int * p34 = &a4;
const volatile int * p41 = &a1;
const volatile int * p42 = &a2;
const volatile int * p43 = &a3;
const volatile int * p44 = &a4;
}
That requirement allows to overload functions depending on the pointer types (of a function parameter), that differ with qualifiers for the data pointed to. It’s about being able to overload for the data types pointed to. Here’s an example:
#include <iostream>
#include <utility>
void foo(int *)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
void foo(const int *)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int main()
{
int i = 1;
foo(&i);
foo(&std::as_const(i));
}
A reference either names some data (e.g., a temporary, a table
element) or is an alias to some other variable. A reference type does
not have qualifiers, because it’s supposed to represent verbatim its
initializing expression. What’s more, a reference can only be
initialized, and later cannot be changed (so that it names some
different data), so a const top-level qualifier would make no
difference. For instance, type int & const is wrong.
Therefore a function cannot be overloaded for reference types (of a function parameter), that would differ with the top-level qualifiers. It’s just impossible: we cannot declare a function with a parameter of a cv-qualified reference type.
Just as for a pointer type, for a reference type too, on the left of
the top-level & declarator, we can put the top-level qualifiers
for the type refered to. We can call these qualifiers lower-level,
if we talk about the reference type.
A requirement for the reference type. The lower-level qualifiers of a reference type must include the top-level qualifiers of the type refered to.
Oto przykład:
int main()
{
int a1 = 1;
const int a2 = 2;
volatile int a3 = 3;
const volatile int a4 = 4;
// The commented lines below would bypass the qualifiers.
int & p11 = a1;
// int & p12 = a2;
// int & p13 = a3;
// int & p14 = a4;
const int & p21 = a1;
const int & p22 = a2;
// const int & p23 = a3;
// const int & p24 = a4;
volatile int & p31 = a1;
// volatile int & p32 = a2;
volatile int & p33 = a3;
// volatile int & p34 = a4;
const volatile int & p41 = a1;
const volatile int & p42 = a2;
const volatile int & p43 = a3;
const volatile int & p44 = a4;
}
That requirement allows to overload functions depending on the reference types (of a function parameter), that differ with qualifiers for the data refered to. It’s about being able to overload for the data types refered to. Here’s an example:
#include <iostream>
#include <utility>
void foo(int &)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
void foo(const int &)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int main()
{
int i = 1;
foo(i);
foo(std::as_const(i));
}
Every expression in C++ is of non-reference type, and so the type of
a function argument is non-reference, even if the expression is a
reference name. The standard says ([expr.type]), that a compiler
removes the top-level & declarator from the expression type before
the expression is further evaluated.
For every kind of a template parameter, a compiler can deduce arguments. We usually want the compiler to deduce arguments of the type kind.
A template argument of a type kind is deduced for a template parameter of a type kind. This deduction is the most complex (in comparison with the value and template kinds), because it considers:
the type of a function parameter, that can only be:
regular (non-pointer and non-reference) for passing by value,
pointer for passing by pointer (that is just passing by value),
reference for passing by reference;
the type of a function argument, especially when the argument is:
a function,
an array;
the category of a function argument (discussed in a later topic).
Depending on the function parameter type (regular, pointer, reference) different deduction rules are used (which follow from the basic rule without type conversion), where the deduced type can differ from the function argument type only with the top-level qualifiers and declarators. The deduced type is never a reference type, because a function argument is never of a reference type.
Rule. The deduced template argument is the type of the function argument with the top-level qualifiers dropped.
It’s about the value of a function argument being copied to a function parameter (when passing by value). Deduction doesn’t have to care about the (top-level) type qualifiers, because in the function body we work with a copy. In the parameter definition we can put (top-level) qualifiers to have a compiler watch over this parameter.
Example:
#include <iostream>
template <typename T>
void
foo(T t)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
// We cannot overload templates with parameters of regular types that
// differ with qualifiers.
// template <typename T>
// void
// foo(const T t)
// {
// }
int
main()
{
int w = 1;
const int x = 2;
volatile int y = 3;
const volatile int z = 4;
foo(w);
foo(x);
foo(y);
foo(z);
}
Defining a function parameter this way (that looks regular), we can pass an argument of a pointer type, because the deduced type will be pointer:
#include <iostream>
template <typename T>
void
foo(const T t)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int
main()
{
int i = 1;
int const volatile * p1 = &i;
int volatile * const p2 = &i;
int const * volatile p3 = &i;
int * const volatile p4 = &i;
foo(p1);
foo(p2);
foo(p3);
foo(p4);
}
Rule. The deduced template argument is the type of the function
argument with the top-level declarator * and qualifiers dropped.
Top-level qualifiers for the type pointed to are also dropped if
they are present in the type definition of a function parameter.
Explanation:
Top-level declarator * is dropped, because it’s already present in
the type definition of the function parameter.
Top-level qualifiers are dropped, because they have no meaning for the function that works on a copy of an argument value (just as for a function parameter of a regular type).
If in the type definition of a function parameter we put top-level qualifiers for the type pointed to, then these qualifiers will be dropped in the deduced type. If we didn’t put them, then the qualifiers (for the type pointed to) defined by the type of the function argument will be propagated to the deduced type, so that a parameter can be initialized.
#include <iostream>
template <typename T>
void
foo(T *t)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
// Can't overload for pointer types of different top-level qualifiers.
// template <typename T>
// void
// foo(T * const t)
// {
// std::cout << __PRETTY_FUNCTION__ << std::endl;
// }
template <typename T>
void
foo(T volatile *t)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int
main()
{
int i = 1;
int const volatile * p1 = &i;
int volatile * const p2 = &i;
int const * volatile p3 = &i;
int * const volatile p4 = &i;
foo(p1);
foo(p2);
foo(p3);
foo(p4);
}
Rule. The deduced template argument is the type of the function argument with those top-level qualifiers dropped that have been put as top-level in the definition of the data type that a reference parameter refers to.
It’s about making the initialization of a reference parameter possible: if the type of a function argument is const (or volatile), then a reference must refer to the cost (or volatile) data. Let’s recall that a function argument is never of a reference type.
Przykład:
#include <iostream>
template <typename T>
void
foo(T &t)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
// Can't overload for reference types of different top-level
// qualifiers because no such thing exists!
// template <typename T>
// void
// foo(T & const t)
// {
// std::cout << __PRETTY_FUNCTION__ << std::endl;
// }
template <typename T>
void
foo(T volatile &t)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int
main()
{
int w = 1;
const int x = 2;
volatile int y = 3;
const volatile int z = 4;
foo(w);
foo(x);
foo(y);
foo(z);
}
We can pass a function by:
reference,
pointer,
but not by value.
We can pass a function by reference to a function template using the
reference type of a function parameter, i.e., F &, where F is a
template parameter of the type kind. The deduced template argument is
the type of the function passed. Example:
#include <iostream>
void
hello()
{
std::cout << "Hello World!\n";
}
template <typename F>
void
foo(F &f)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
f();
}
void
goo(void (&f)())
{
f();
}
int
main()
{
foo(hello);
goo(hello);
// An expression of a pointer type is an rvalue, and so "&hello"
// below is an rvalue. Since the hello function is of a non-const
// type (there is no such thing as a const non-member function), the
// constness cannot be deduced, and therefore the non-const
// reference parameter "F &" cannot be initialized with an rvalue.
// It would compile if the foo parameter was a const reference.
// foo(&hello);
// What's that!?
foo(*hello);
// It's the same as the following, because in "*hello", "hello"
// decays into "&hello".
foo(*&hello);
}
We can pass a function by pointer to a function template using a
pointer type of a function parameter, i.e., type F *, where F is a
template parameter of the type kind. The deduced template argument is
the type of the function passed. Example:
#include <iostream>
void
hello()
{
std::cout << "Hello World!\n";
}
template <typename F>
void
foo(F *f)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
f();
}
void
goo(void (*f)())
{
f();
}
int
main()
{
foo(&hello);
goo(&hello);
// The following has the same effect as the above, because the
// function name decays (is converted) to the function pointer.
foo(hello);
goo(hello);
}
From the C language: a function name can decay to a pointer to the function. The decay takes place in the example above, where a function name is an argument of a call. A function name does not decay, if the function is passed by reference.
We can pass a function by pointer to a function template using a
regular (non-reference and non-pointer) type of a function parameter,
i.e., F, where F is a template parameter of the type kind. Then
the deduced template argument is the pointer type to the function
passed, because the function name decays to a pointer. Let’s note
that it’s not passing a function by value, because there is no such
thing. Example:
#include <iostream>
void
hello()
{
std::cout << "Hello World!\n";
}
template <typename F>
void
foo(F f)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
f();
}
void
goo(void (*f)())
{
f();
}
int
main()
{
foo(&hello);
goo(&hello);
// The following has the same effect as the above, because the
// function name decays (is converted) to the function pointer.
foo(hello);
goo(hello);
}
We can pass an array by:
reference,
pointer,
but not by value.
We can pass an array by reference to a function template using the
reference type of a function parameter, i.e., A &, where A is a
template parameter of the type kind. The deduced template argument is
the type of the array passed. Example:
#include <iostream>
template <typename A>
void
foo(A &a)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
void
goo(int (&a)[3])
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int
main()
{
int a[] = {1, 2, 3};
foo(a);
goo(a);
}
We can pass an array by pointer to a function template using the
pointer type of a function parameter, i.e., A *, where A is a
template parameter of the type kind. The deduced template argument is
the type of the array passed. Example:
#include <iostream>
template <typename A>
void
foo(A *a)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
void
goo(int (*a)[3])
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int
main()
{
int a[] = {1, 2, 3};
// foo(a);
// goo(a);
foo(&a);
goo(&a);
}
If the type of a function parameter in a function template is regular (non-reference and non-pointer), and the function argument is the array name, then a poiner to the first array element (and not a pointer to the array) is passed to the function, because:
the array decays to the pointer to the first array element,
the deduced template argument is the pointer type to an array element.
Here’s an example with the decay:
#include <iostream>
template <typename A>
void
foo(A a[])
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
void
goo(int a[])
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int
main()
{
int a[] = {1, 2, 3};
foo(a);
goo(a);
// foo(&a);
// goo(&a);
}
The deduction of a template argument of the value kind is far easier than of the type kind. It’s even hard to talk about deduction, because the template argument is extracted (taken) from the type of an argument accepted by a function that we define as the type of the function parameter. In the definition of a function parameter type we use the template parameter for which an argument will be deduced.
The only types of a function argument, which can be used to deduce a value template argument, are:
an array type,
a template type.
Here’s an example:
#include <iostream>
template <typename T, unsigned I>
void
roo(T (&)[I])
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
template <typename T, unsigned I>
void
poo(T (*)[I])
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int
main()
{
const char *t1[] = {"Hello", "World!"};
int t2[] = {1, 2, 3, 4, 5};
roo(t1);
roo(t2);
poo(&t1);
poo(&t2);
}
The types of the value parameters of both templates must agree. These two templates are:
the function template: the value parameter of this template has an argument deduced,
the type template: this template is used in the definition of the function parameter.
For instance, a value parameter I must be of type std::size_t,
because the value parameter of type template std::array is of that
type:
#include <array>
#include <iostream>
using namespace std;
template <typename T, std::size_t I>
void
foo(const array<T, I> &)
{
cout << __PRETTY_FUNCTION__ << endl;
cout << "The array has " << I << " elements.\n";
}
int
main()
{
foo(array{"Hello ", "World!"});
foo(array{1, 2, 3, 4, 5});
}
In the above examples, we used just a single function parameter, and so a template parameter could have been used in at most one type definition of a function parameter. However, there can be any number of function parameters, and a template parameter can be used in the type definition of every function parameter. How are then the template arguments deduced?
Then the template arguments are deduced independently for every pair of a parameter and an argument of a function. For every pair, only those arguments are deduced, whose parameters are used in the type definition of a function parameter. If some argument was deduced more than once (i.e., for different pairs), then it must be the same, otherwise deduction fails.
Deduction forbids type conversion. In the example below, different arguments are deduced, because the types of function arguments are different and cannot be converted. Therefore deduction fails in the example below:
template <typename T>
T
max_ref(const T &a, const T &b)
{
return b < a ? a : b;
}
template <typename T>
T
max_val(T a, T b)
{
return b < a ? a : b;
}
double
MAX(const double &a, const double &b)
{
return b < a ? a : b;
}
int
main()
{
// 1 is converted to .1 (which is a temporary, an rvalue), so that a
// const reference to double (the first parameter) can bind to it.
MAX(1, .1);
// The following call to a template function fails, because:
//
// * no conversion is allowed, so 1 cannot be converted to .1.
//
// * T is first deduced int, then double, so deduction fails.
// max_ref(1, .1);
// max_val(1, .1);
// No deduction takes place.
max_ref<int>(1, '1');
max_ref<double>(1, .1);
max_val<int>(1, '1');
max_val<double>(1, .1);
}
A template argument can be either deduced or given (either explicitly or implicitly).
Template argument deduction depends on the type of a function parameter and the type of a function argument.
To understand deduction, we have to know the details about the initialization of variables, pointers and references.
What does the deduction depend on?
Does the deduction depend on the type of a return value?
What is the decay of an array?