A template can be of a:
function,
class, struct, union,
member (a member function, a member field),
type alias,
variable.
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 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>
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.,:
default-construct a value of type T
,
add, using operator+
, the values of type T
,
dereference, using operator&
, a value of type T
,
pass to some function, e.g., push_back
, a value of type T
,
output to std::ostream
a value of type T
using operator<<
.
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!"));
}
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);
}
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);
}
Arguments can be:
deduced by a compiler (most frequenlty used),
explicitly given by a programmer (sometimes indispensable),
defaulted by a programmer (sometimes useful).
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.
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:
we want different arguments to those a compiler would deduce,
we have to explicitly provide the arguments because the compiler would be unable to deduce them.
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 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!");
}
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>();
}
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{});
}
Templates are the basis of generic programming in C++.
Template parameters can be of three kinds: type, value and template.
Among templates, function templates are the most interesting and complex.
What is the type template parameter?
What can a template be of?
Template argument vs parameter.