cpp

Templates

A template can be of a:

A template declaration or definition begins with the template keyword, and it has this syntax:

template <parameter list>

We say a template is parametrized, because it has parameters that we call template parameters. A template parameter is defined in the parameter list (parameter list above) and has a name, that we then use in the template declaration or definition.

When we use a template (e.g., we call a function defined with a template), then after the template name we can provide template arguments in <>. Instantiation of a template substitutes a template argument for a template parameter. We also say that a parameter accepts an argument.

The terms of a template parameter and a template argument are analogous to the terms of a function parameter and a function argument, but this analogy is only skin-deep. Initialization of a function parameter with a function argument has many details (e.g., type conversion, reference initialization), that do not apply to substitution. Substitution only checks whether the argument is valid, i.e., that it is a type, a value or a type template, as stipulated by the parameter kind. Bottom line: substitution is not initialization.

Template parameters

Template parameters are defined in a parameter list, where they are comma-separated. A parameter is defined by a kind and an optional name. There are three kinds: type, value, and template. The example below has three parameters: T of the type kind, an unnamed parameter of the value kind, and C of the template kind.

template <typename T, int, template<typename> typename C>

Type parameter

The template parameter of the type kind in short we can call the type template parameter. It’s the most common kind. We define a type parameter with typename T, where typename says it’s a type parameter, and T is the name of the parameter. We can also define equivalently a type parameter with class T, but typename T is preferred in modern C++.

Instantiation can substitute T with any concrete type: a built-in type (e.g., int), a user-defined type (e.g., A), a template type (e.g., vector<int>), even void. Concrete, meaning not a type template. T doesn’t have to meet any requirements, e.g., inheriting from an interface class. The requirements on type T follow from how we use the type in the template definition, i.e., whether we, e.g.,:

This is an example of a function template with a type parameter, where the compiler is able to deduce the template argument, so that we do not have to provide it explictly when calling the function:

#include <iostream>
#include <string>

using namespace std;

template <typename T>
void
print(const T &t)
{
  cout << t << endl;
}

int
main()
{
  print(1);
  print(0.5);
  print("Hello!");
  print(string("Hello!"));
}

Value parameter

Also known as the non-type parameter. A parameter of this kind we define with some_type I, where some_type is a type, e.g., int. Type some_type cannot be any, only some types allowed, with integer types being the most popular. Instantiation substitutes the parameter name with a value of type some_type, e.g., 1 for a template parameter defined as int I.

An example definition of a value template parameter:

template <int N>

This is an example of a function template with a value parameter N whose argument must be explicitly provided because a compiler is unable to deduce it:

#include <iostream>

template <int N>
void
print()
{
  std::cout << N << std::endl;
}

int
main()
{
  print<100>();
}

Here we have two overloads of function templates (overloads because they have the same name). The second template has a value parameter N and a type parameter T whose argument can be deduced:

#include <iostream>

template <typename T>
void print(const T &t)
{
  std::cout << t << '\n';
}

template <int N, typename T>
void print(const T &t)
{
  for(int n = N; n--;)
    print(t);
}

int
main()
{
  print("Hello!");
  print<10>("World!");
}

This is an example of a recursive function template, where the recursion is terminated by the compile-time conditional statement if constexpr:

#include <iostream>

template <typename T>
void print(const T &t)
{
  std::cout << t << '\n';
}

template <int N, typename T>
void print(const T &t)
{
  if constexpr (N)
    {
      print(t);
      print<N - 1>(t);
    }
}

int
main()
{
  print("Hello!");
  print<10>(1);
}

Among the allowed types for value parameters are function pointers and references:

#include <iostream>

template <typename T>
void
debug(T a, T b)
{
  std::cout << a << b;
}

void
nodebug(int a, int b)
{
}

// The version with the callback reference.
template <void (&F)(int, int) = nodebug>
void roo(int a, int b)
{
  F(a, b);
}

// The version with the callback pointer.
template <void (*F)(int, int) = nodebug>
void poo(int a, int b)
{
  F(a, b);
}

int
main()
{
  // The nodebug function is used.
  roo(1, 2);
  poo(1, 2);

  // The baseline code, the same as in the debug function.
  std::cout << 1 << 2;

  // For GCC 13.1.0 with -O1, the following line produces the same
  // assembly code as the line above.
  roo<debug>(1, 2);
  poo<debug>(1, 2);
}

Template parameter

A template paramter of the template kind accepts (as its argument) a type template of the required interface. It’s also known as the template template parameter. The definition of the template parameter T defines (with the parameter list parameter-list) the interface of the acceptable type templates:

template <parameter-list> typename T

Here we use parameter T:

template <template <parameter-list> typename T>

In this example, the template parameter C accepts only a template type that, in turn, accepts a type argument and the value argument:

#include <array>
#include <iostream>

using namespace std;

template <template <typename, long unsigned> typename C, typename T>
void
foo(T t)
{
  cout << __PRETTY_FUNCTION__ << endl;
  C<T, 1> c1 = {t};
  C<T, 2> c2 = {t, t};
}

int
main()
{
  foo<array>(1);

  // This is cool: pointer to an instantiated function template.  We
  // instantiate the function right here, because we point to it.
  void (*fp)(double) = foo<array, double>;
  fp(.1);
}

A compiler (Clang, GCC) replaces __PRETTY_FUNCTION__ with the function name and the template arguments, so that we can examine how the function template was instantiated.

A template parameter allows to deduce the arguments of the instantiated template type:

#include <array>
#include <iostream>

using namespace std;

template <template <typename, std::size_t> typename C, std::size_t I>
void
foo(const C<int, I> &c)
{
  cout << __PRETTY_FUNCTION__ << endl;
  cout << "Got " << I << " elements\n";
}

int
main()
{
  foo(array{1, 2, 3});
}

A template parameter breaks circular dependency between template types:

#include <vector>

using namespace std;

// We want to implement a container of elements, where the elements
// have a reference to the container, i.e., an element knows who owns
// it.  We have two element types:
//
// A - disfunctional because of the circular dependency,
//
// B - working fine with the circular dependency resolved.

// Those elements want to have a reference to the object that owns
// them.  And that's a problem, because this creates a circular
// dependency.
template <typename T>
struct A
{
  T &m_t;

  A(T &t): m_t(t)
  {
  }
};

// We can break the circular dependency with the template-kind
// parameter of a template and injected class names.  This is not a
// perfect solution, because we require the owning type T to be
// templated with a single argument, so other types will not be
// accepted.
template <template <typename...> typename T>
struct B
{
  // "B" is an injected class name, i.e., we do not have to write
  // "B<T>" and that helps us break the circular dependency.
  T<B> &m_t;

  B(T<B> &t): m_t(t)
  {
  }
};

int
main()
{
  // We can't possibly define a container of elements A because of the
  // circular dependency.
  // vector<A<vector<A<vector<A<vector... a1;
  // vector<A<vector>> a2;

  // Is the initialization ill-formed or undefined-behaved?  Note that
  // we use uninitialized container "b" to initialize element B(b).
  vector<B<vector>> b = {B(b)};
  b.push_back(B(b));
  // Looks like a snake eating its own tail.
  b.emplace_back(b);
}

Template arguments

Arguments can be:

This example demonstrates the above functionality:

#include <iostream>

using namespace std;

template <typename T = int>
void
print(T t = {})
{
  cout << __PRETTY_FUNCTION__ << endl;
  cout << t << endl;
}

int
main()
{
  // A template argument is deduced.
  print("Hello");           // T = const char *
  print(string("World!"));  // T = string 
  print(2020);              // T = int
  print(.1);                // T = double

  // We explicitely give a template argument.
  print<double>(1);         // T = double
  print<string>("Hello!");  // T = string
  print<double>();          // T = double
  // This one produces a warning, so I commented it out.
  // print<int>(1.2);          // T = int

  // We use the default template argument (int), and a default value
  // for a call argument ({}, which is 0 for int).
  print();
}

Before we get to deduction, first we discuss the explicit and default template arguments.

Explicitly given template arguments

When we use the containers of the standard library, we can explicitly give the arguments as part of the container type in <>, i.e., using the syntax type<argument list>:

#include <vector>

int
main()
{
  std::vector<int> v1{3, 1, 2};

  // A compiler can deduce the integer type from the initializer list.
  std::vector v2{3, 1, 2};
}

We can use that syntax when calling a template function (that we have already used above) which is indispensable in two cases:

A compiler deduces the template arguments based on the expressions that are passed as function arguments (when we call a function) or constructor arguments (when we create an object). When a compiler is unable to deduce the template arguments, we have to provide them explicitly.

An example below shows the implementation of an object factory. The argument of calling factory is passed to the constructor of the object whose type is given by the template parameter T. The compiler is unable to deduce the argument for T, so we have to provide it explicitly.

#include <iostream>

using namespace std;

struct A
{
  A(int i)
  {
    cout << "ctor A: " << i << endl;
  }
};

struct B
{
  // The constructor is templated, even though the struct is not.
  template <typename T>
  B(T t)
  {
    cout << "ctor B: " << t << endl;
  }
};

template <typename T, typename A>
T
factory(A a)
{
  return T(a);
}

int
main()
{
  // Just as for std::make_unique.
  factory<A>(1);
  factory<B>(1.1);
  factory<B>("Hello World!");
}

The order of arguments

The order of template arguments matters (exactly the same as in the case of function arguments) because they are positional, i.e., the argument position determines the parameter. And so if we want to provide the second argument, we have to provide the first.

Let’s swap the parameters in the factory example. Now that we provide the argument for parameter T as the second, we also have to provide the argument for A as the first. Prior to the change, the argument for A was deduced.

#include <iostream>

using namespace std;

struct A
{
  A(int i)
  {
    cout << "ctor A: " << i << endl;
  }
};

struct B
{
  template <typename T>
  B(T t)
  {
    cout << "ctor B: " << t << endl;
  }
};

template <typename A, typename T>
T
factory(A a)
{
  return T(a);
}

int
main()
{
  factory<int, A>(1);
  factory<double, B>(1.1);
  factory<const char *, B>("Hello World!");
}

Default template arguments

A template parameter (of any kind) can have a default argument that will be used if an argument was not explicitly given or a compiler is unable to deduce it. A default argument is optional.

We define a default argument after a parameter name using =. Here’s an example:

#include <deque>
#include <iostream>
#include <vector>

using namespace std;

template <template <typename...> typename C = std::vector,
          typename T = int, unsigned I = 10>
C<T>
container_factory()
{
  cout << __PRETTY_FUNCTION__ << endl;
  return C<T>(I);
}

int
main()
{
  container_factory();
  container_factory<std::deque>();
  container_factory<std::vector, double>();
  container_factory<std::deque, bool, 1>();
}

A default callable

Sometimes we need to pass a callable to some function but that may not be always necessary. We do not want to pass a pointer and check at run-time whether it’s a nullptr, because it’s inefficient and uninteresting. We would like to have the callable inlined, and if it’s not required, performance should not deteriorate. A default template argument is useful for that.

Solution: a callable type is a template parameter with a default argument that is an empty callable, i.e., a type with an empty implementation of the call operator. We also have to provide a default value of a callable (a default function argument), i.e., {} (default-construct the argument). That’s a nice example:

#include <iostream>

struct empty_callable
{
  void
  operator()()
  {
  }
};

struct it
{
  void
  operator()()
  {
    std::cout << "Done it.\n";
  }
};

template <typename C = empty_callable>
void
do_sth(C c = {})
{
  c();
}


int
main()
{
  // Do nothing.
  do_sth();
  // Do it.
  do_sth(it{});
}

Conclusion

Quiz