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

Wskaźnikowy typ parametru funkcji

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:

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

Referencyjny typ parametru funkcji

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

Przekazywanie funkcji

Funkcję możemy przekazać przez:

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

Przekazywanie tablic języka C

Tablicę języka C możemy przekazać do funkcji szablonowej przez:

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:

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

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

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

Dowolny typ szablonowy

Typy wartościowych parametrów obu szablonów muszą się zgadzać. Te oba szablony to:

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

Wiele parametrów funkcji szablonowej

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

Podsumowanie

Quiz