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);
}
Zasada. Wywnioskowany argument szablonu jest typem argumentu
funkcji z pominięciem deklaratora * i kwalifikatorów najwyższego
rzędu. Kwalifikatory najwyższego rzędu dla typu wskazywanych danych
też są pomijane, jeżeli znajdują się w definicji typu parametru
funkcji.
Wyjaśnienie:
Deklarator * najwyższego rzędu jest pomijany, bo on już jest w
definicji typu parametru funkcji.
Kwalifikatory najwyższego rzędu są pomijane, bo nie mają one znaczenia dla funkcji, która działa na kopii wartości argumentu przekazanego do niej (tak jak w przypadku zwykłego typu parametru funkcji).
Jeżeli w definicji typu parametru funkcji podamy kwalifikatory najwyższego rzędu dla typu wskazywanych danych, to te kwalifikatory będą pominięte we wywnioskowanym typie. Jeżeli nie zostały podane, to kwalifikatory (typu wskazywanych danych) zdefiniowane przez typ argumentu funkcji będą propagowane do wywnioskowanego typu, co pozwala na inicjalizację parametru funkcji.
#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);
}
Zasada. Wywnioskowany argument szablonu jest typem argumentu funkcji z pominięciem tych kwalifikatorów najwyższego rzędu, które zostały podane w definicji typu danych, do których referencyjny parametr funkcji się odnosi.
Chodzi o to, żeby referencyjny parametr funkcji rzeczywiście mógł być zainicjalizowany: jeżeli typ argumentu wywołania jest stały (bądź ulotny), to referencja musi odnosić się do danej typu stałego (bądź ulotnego). Pamiętajmy, że typ argumentu nigdy nie jest referencyjny.
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);
}
Funkcję możemy przekazać przez:
referencję,
wskaźnik,
ale nie wartość.
Funkcję możemy przekazać przez referencję używając referencyjnego
typu parametru funkcji, a dokładnie typu F &, gdzie F jest typowym
parametrem szablonu. Wywnioskowanym argumentem szablonu będzie typ
referencji na przekazywaną funkcję. Przykład:
#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 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);
}
Funkcję możemy przekazać przez wskaźnik używając wskaźnikowego typu
parametru funkcji szablonowej, a dokładnie typu F *, gdzie F jest
typowym parametrem szablonu. Wywnioskowanym argumentem szablonu
będzie typ przekazywanej funkcji. Przykład:
#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);
}
Zamianę nazwy funkcji na wskaźnik do niej nazywamy rozpadem funkcji na wskaźnik (ang. decay), który pochodzi z języka C. Z rozpadu skorzystaliśmy wyżej podając nazwę funkcji jako argument wywołania funkcji szablonowej.
Funkcję możemy także przekazać przez wskaźnik używając zwykłego
(niereferencyjnego i niewskaźnikowego) typu parametru funkcji, a
dokładnie typu F, gdzie F jest typowym parametrem szablonu. Wtedy
wywnioskowanym argumentem szablonu będzie typ wskaźnikowy na funkcję,
bo nazwa funkcji rozpada się na wskaźnik. Zwróćmy uwagę, że to nie
jest przekazywanie funkcji przez wartość, bo czegoś takiego nie ma.
Przykład:
#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);
}
Tablicę języka C możemy przekazać do funkcji szablonowej przez:
referencję,
wskaźnik,
ale nie wartość.
Tablicę możemy przekazać przez referencję używając referencyjnego
typu parametru funkcji szablonowej, a dokładnie typu A &, gdzie A
jest typowym parametrem szablonu. Wywnioskowanym argumentem będzie
typ tablicy. Przykład:
#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);
}
Tablicę możemy przekazać przez wskaźnik używając wskaźnikowego typu
parametru funkcji szablonowej, a dokładnie typu A *, gdzie A jest
typowym parametrem szablonu. Wywnioskowanym argumentem będzie typ
tablicy. Przykład:
#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);
}
Jeżeli typem parametru funkcji szablonowej jest zwykły typ (niereferencyjny i niewskaźnikowy), a argumentem wywołania funkcji będzie nazwa tablicy, to do funkcji zostanie przekazany wskaźnik na pierwszy element tablicy (a nie wskaźnik na tablicę), bo:
tablica rozpadnie się na wskaźnik na pierwszy element tablicy,
wywnioskowanym argumentem będzie typ wskaźnikowy na element tablicy.
Oto przykład z rozpadem:
#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);
}
Wartościowy argument szablonu jest wnioskowany tylko na podstawie typu argumentu wywołania funkcji, z którego można ten argument wywnioskować. Częścią typu argumentu wywołania funkcji musi być pewna wartość, którą potrzebujemy, i która staje się wywnioskowanym wartościowym argumentem.
Jedynymi typami argumentu wywołania funkcji, na podstawie których możemy wywnioskować wartościowy argument szablonu, to:
typ tablicy języka C,
dowolny typ szablonowy.
Oto przykład:
#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);
}
Typy wartościowych parametrów obu szablonów muszą się zgadzać. Te oba szablony to:
szablon funkcji: wartościowy parametr tego szablonu ma wywnioskowany argument,
szablon typu: tego szablonowego typu jest argument wywołania funkcji.
Na przykład, wartościowy parametr I musi mieć typ std::size_t, bo
takiego typu jest wartościowy parametr typu szablonowego std::array:
#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});
}
W przykładach wyżej używaliśmy tylko jednego parametru funkcji szablonowej, więc parametry szablonu mogły być użyte w co najwyżej jednej definicji typu parametru funkcji szablonowej. Parametrów funkcji szablonowej może być jednak dowolna liczba, a parametr szablonu może być użyty w dowolnej liczbie definicji typów parametrów funkcji szablonowej. Jak wtedy wnioskowane są argumenty szablonu?
Wtedy wnioskowanie argumentów szablonu odbywa się niezależnie dla każdej pary parametru funkcji i argumentu wywołania. Dla każdej pary wnioskowane są argumenty dla parametrów szablonu, które zostały użyte w definicji typu tego parametru funkcji. Jeżeli jakiś argument został wywnioskowany więcej niż raz (czyli dla różnych par), to musi on być taki sam, w przeciwnym razie wnioskowanie nie udaje się.
Podczas wnioskowania nie jest dopuszczalna konwersja typów. W przykładzie niżej wnioskowane są różne argumenty, bo nie jest dopuszczalna konwersja różnych typów argumentów wywołania funkcji. Zatem w poniższym przykładzie wnioskowanie nie udaje się:
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);
}
Argument szablonu może być wywnioskowany albo podany (jawnie albo domyślnie).
Wnioskowanie argumentów szablonu zależy od typu parametru funkcji i typu argumentu wywołania funkcji.
Żeby zrozumieć wnioskowanie, należy znać szczegóły dotyczące inicjalizacji zmiennych, wskaźników i referencji.
Na podstawie czego wnioskowane są argumenty szablonu?
Czy wnioskowanie uwzględnia typ wartości zwracanej przez funkcję?
Co to jest rozpad tablicy?