cpp

Introduction

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:

Deduction basics

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:

How an argument category affects deduction is quite complex, and therefore is described in a separate topic on perfect argument forwarding.

The simplest case

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.

The basic rule without type conversion

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.

Examples

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.

Note: “no conversion”

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.

Top-level

The place of a qualifier or a declarator in a defined type affects:

Type qualifiers and declarators can be top-level.

Declarators

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 &.

Qualifiers

Type qualifiers (const and volatile) can be top-level for any type except a reference type.

Regular 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;
}

Pointer type

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));
}

Reference type

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));
}

Expression type: never reference!

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.

Deduction

For every kind of a template parameter, a compiler can deduce arguments. We usually want the compiler to deduce arguments of the type kind.

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:

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.

A regular type of a function parameter

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);
}

A pointer type of a function parameter

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:

#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);
}

A reference type of a function parameter

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);
}

Passing a function

We can pass a function by:

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);
}

Passing an array

We can pass an array by:

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:

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);
}

Wartościowy argument szablonu

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

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);
}

A template type

The types of the value parameters of both templates must agree. These two templates are:

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});
}

Many function parameters

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);
}

Conclusion

Quiz