Chapter 2: Template Fundamentals

In the previous chapter, we saw a short introduction to templates. What they are, how they are helpful, pros and cons for using templates, and, also, a few examples of function and class templates. In this chapter, we will explore this area in detail, and look at aspects such as template parameters, instantiation, specializations, aliases, and more. The main topics that you will learn about from this chapter are as follows:

  • How to define function templates, class templates, variable templates, and alias templates
  • What kinds of template parameters exist?
  • What is template instantiation?
  • What is template specialization?
  • How to use generic lambdas and lambda templates

By the end of this chapter, you will be familiar with the core fundamentals of templates in C++ and be able to understand large areas of template code and also write templates by yourself.

To start this chapter, we will explore the details of defining and using function templates.

Defining function templates

Function templates are defined in a similar way to regular functions, except that the function declaration is preceded by the keyword template followed by a list of template parameters between angle brackets. The following is a simple example of a function template:

template <typename T>
T add(T const a, T const b)
{
   return a + b;
}

This function has two parameters, called a and b, both of the same T type. This type is listed in the template parameters list, introduced with the keyword typename or class (the former is used in this example and throughout the book). This function does nothing more than add the two arguments and returns the result of this operation, which should have the same T type.

Function templates are only blueprints for creating actual functions and only exist in source code. Unless explicitly called in your source code, the function templates will not be present in the compiled executable. However, when the compiler encounters a call to a function template and is able to match the supplied arguments and their types to a function template's parameters, it generates an actual function from the template and the arguments used to invoke it. To understand this, let's look at some examples:

auto a = add(42, 21);

In this snippet, we call the add function with two int parameters, 42 and 21. The compiler is able to deduce the template parameter T from the type of the supplied arguments, making it unnecessary to explicitly provide it. However, the following two invocations are also possible, and, in fact, identical to the earlier one:

auto a = add<int>(42, 21);
auto a = add<>(42, 21);

From this invocation, the compiler will generate the following function (keep in mind that the actual code may differ for various compilers):

int add(const int a, const int b)
{
  return a + b;
}

However, if we change the call to the following form, we explicitly provide the argument for the template parameter T, as the short type:

auto b = add<short>(42, 21);

In this case, the compiler will generate another instantiation of this function, with short instead of int. This new instantiation would look as follows:

short add(const short a, const int b)
{
  return static_cast<short>(a + b);
}

If the type of the two parameters is ambiguous, the compiler will not be able to deduce them automatically. This is the case with the following invocation:

auto d = add(41.0, 21);

In this example, 41.0 is a double but 21 is an int. The add function template has two parameters of the same type, so the compiler is not able to match it with the supplied arguments and will issue an error. To avoid this, and suppose you expected it to be instantiated for double, you have to specify the type explicitly, as shown in the following snippet:

auto d = add<double>(41.0, 21);

As long as the two arguments have the same type and the + operator is available for the type of the arguments, you can call the function template add in the ways shown previously. However, if the + operator is not available, then the compiler will not be able to generate an instantiation, even if the template parameters are correctly resolved. This is shown in the following snippet:

class foo
{
   int value;
public:
   explicit foo(int const i):value(i)
   { }
   explicit operator int() const { return value; }
};
auto f = add(foo(42), foo(41));

In this case, the compiler will issue an error that a binary + operator is not found for arguments of type foo. Of course, the actual message differs for different compilers, which is the case for all errors. To make it possible to call add for arguments of type foo, you'd have to overload the + operator for this type. A possible implementation is the following:

foo operator+(foo const a, foo const b)
{
  return foo((int)a + (int)b);
}

All the examples that we have seen so far represented templates with a single template parameter. However, a template can have any number of parameters and even a variable number of parameters. This latter topic will be addressed in Chapter 3, Variadic Templates. The next function is a function template that has two type template parameters:

template <typename Input, typename Predicate>
int count_if(Input start, Input end, Predicate p)
{
   int total = 0;
   for (Input i = start; i != end; i++)
   {
      if (p(*i))
         total++;
   }
   return total;
}

This function takes two input iterators to the start and end of a range and a predicate and returns the number of elements in the range that match the predicate. This function, at least conceptually, is very similar to the std::count_if general-purpose function from the <algorithm> header in the standard library and you should always prefer to use standard algorithms over hand-crafted implementations. However, for the purpose of this topic, this function is a good example to help you understand how templates work.

We can use the count_if function as follows:

int main()
{
   int arr[]{ 1,1,2,3,5,8,11 };
   int odds = count_if(
                 std::begin(arr), std::end(arr), 
                 [](int const n) { return n % 2 == 1; });
   std::cout << odds << '
';
}

Again, there is no need to explicitly specify the arguments for the type template parameters (the type of the input iterator and the type of the unary predicate) because the compiler is able to infer them from the call.

Although there are more things to learn about function templates, this section provided an introduction to working with them. Let's now learn the basics of defining class templates.

Defining class templates

Class templates are declared in a very similar manner, with the template keyword and the template parameter list preceding the class declaration. We saw the first example in the introductory chapter. The next snippet shows a class template called wrapper. It has a single template parameter, a type called T, that is used as the type for data members, parameters, and function return types:

template <typename T>
class wrapper
{
public:
   wrapper(T const v): value(v)
   { }
   T const& get() const { return value; }
private:
   T value;
};

As long as the class template is not used anywhere in your source code, the compiler will not generate code from it. For that to happen, the class template must be instantiated and all its parameters properly matched to arguments either explicitly, by the user, or implicitly, by the compiler. Examples for instantiating this class template are shown next:

wrapper a(42);           // wraps an int
wrapper<int> b(42);      // wraps an int
wrapper<short> c(42);    // wraps a short
wrapper<double> d(42.0); // wraps a double
wrapper e("42");         // wraps a char const *

The definitions of a and e in this snippet are only valid in C++17 and onward thanks to a feature called class template argument deduction. This feature enables us to use class templates without specifying any template argument, as long as the compiler is able to deduce them all. This will be discussed in Chapter 4, Advanced Template Concepts. Until then, all examples that refer to class templates will explicitly list the arguments, as in wrapper<int> or wrapper<char const*>.

Class templates can be declared without being defined and used in contexts where incomplete types are allowed, such as the declaration of a function, as shown here:

template <typename T>
class wrapper;
void use_foo(wrapper<int>* ptr);

However, a class template must be defined at the point where the template instantiation occurs; otherwise, the compiler will generate an error. This is exemplified with the following snippet:

template <typename T>
class wrapper;                       // OK
void use_wrapper(wrapper<int>* ptr); // OK
int main()
{
   wrapper<int> a(42);            // error, incomplete type
   use_wrapper(&a);
}
template <typename T>
class wrapper
{
   // template definition
};
void use_wrapper(wrapper<int>* ptr)
{
   std::cout << ptr->get() << '
';
}

When declaring the use_wrapper function, the class template wrapper is only declared, but not defined. However, incomplete types are allowed in this context, which makes it all right to use wrapper<T> at this point. However, in the main function we are instantiating an object of the wrapper class template. This will generate a compiler error because at this point the definition of the class template must be available. To fix this particular example, we'd have to move the definition of the main function to the end, after the definition of wrapper and use_wrapper.

In this example, the class template was defined using the class keyword. However, in C++ there is little difference between declaring classes with the class or struct keyword:

  • With struct, the default member access is public, whereas using class is private.
  • With struct, the default access specifier for base-class inheritance is public, whereas using class is private.

You can define class templates using the struct keyword the same way we did here using the class keyword. The differences between classes defined with the struct or the class keyword are also observed for class templates defined with the struct or class keyword.

Classes, whether they are templates or not, may contain member function templates too. The way these are defined is discussed in the next section.

Defining member function templates

So far, we have learned about function templates and class templates. It is possible to define member function templates too, both in non-template classes and class templates. In this section, we will learn how to do this. To understand the differences, let's start with the following example:

template <typename T>
class composition
{
public:
   T add(T const a, T const b)
   {
      return a + b;
   }
};

The composition class is a class template. It has a single member function called add that uses the type parameter T. This class can be used as follows:

composition<int> c;
c.add(41, 21);

We first need to instantiate an object of the composition class. Notice that we must explicitly specify the argument for the type parameter T because the compiler is not able to figure it out by itself (there is no context from which to infer it). When we invoke the add function, we just provide the arguments. Their type, represented by the T type template parameter that was previously resolved to int, is already known. A call such as c.add<int>(42, 21) would trigger a compiler error. The add function is not a function template, but a regular function that is a member of the composition class template.

In the next example, the composition class changes slightly, but significantly. Let's see the definition first:

class composition
{
public:
   template <typename T>
   T add(T const a, T const b)
   {
      return a + b;
   }
};

This time, composition is a non-template class. However, the add function is a function template. Therefore, to call this function, we must do the following:

composition c;
c.add<int>(41, 21);

The explicit specification of the int type for the T type template parameter is redundant since the compiler can deduce it by itself from the arguments of the call. However, it was shown here to better comprehend the differences between these two implementations.

Apart from these two cases, member functions of class templates and member function templates of classes, we can also have member function templates of class templates. In this case, however, the template parameters of the member function template must differ from the template parameters of the class template; otherwise, the compiler will generate an error. Let's return to the wrapper class template example and modify it as follows:

template <typename T>
class wrapper
{
public:
   wrapper(T const v) :value(v)
   {}
   T const& get() const { return value; }
   template <typename U>
   U as() const
   {
      return static_cast<U>(value);
   }
private:
   T value;
};

As you can see here, this implementation features one more member, a function called as. This is a function template and has a type template parameter called U. This function is used to cast the wrapped value from a type T to a type U, and return it to the caller. We can use this implementation as follows:

wrapper<double> a(42.0);
auto d = a.get();       // double
auto n = a.as<int>();   // int

Arguments for the template parameters were specified when instantiating the wrapper class (double) – although in C++17 this is redundant, and when invoking the as function (int) to perform the cast.

Before we continue with other topics such as instantiation, specialization, and other forms of templates, including variables and aliases, it's important that we take the time to learn more about template parameters. This will make the subject of the next section.

Understanding template parameters

So far in the book, we have seen multiple examples of templates with one or more parameters. In all these examples, the parameters represented types supplied at instantiation, either explicitly by the user, or implicitly by the compiler when it could deduce them. These kinds of parameters are called type template parameters. However, templates can also have non-type template parameters and template template parameters. In the following sections, we'll explore all of them.

Type template parameters

As already mentioned, these are parameters representing types supplied as arguments during the template instantiation. They are introduced either with the typename or the class keyword. There is no difference between using these two keywords. A type template parameter can have a default value, which is a type. This is specified the same way you would specify a default value for a function parameter. Examples for these are shown in the following snippet:

template <typename T>
class wrapper { /* ... */ };
template <typename T = int>
class wrapper { /* ... */ };

The name of the type template parameter can be omitted, which can be useful in forwarding declarations:

template <typename>
class wrapper;
template <typename = int>
class wrapper;

C++11 has introduced variadic templates, which are templates with a variable number of arguments. A template parameter that accepts zero or more arguments is called a parameter pack. A type template parameter pack has the following form:

template <typename... T>
class wrapper { /* ... */ };

Variadic templates will be addressed in Chapter 3, Variadic Templates. Therefore, we will not get into details about these kinds of parameters at this point.

C++20 has introduced concepts and constraints. Constraints specify requirements on template arguments. A named set of constraints is called a concept. Concepts can be specified as type template parameters. However, the syntax is a little bit different. The name of the concept (followed by a list of template arguments in angle brackets if the case) is used instead of the typename or the class keyword. Examples, including concepts with a default value and constrained type template parameter pack, are shown as follows:

template <WrappableType T>
class wrapper { /* ... */ };
template <WrappableType T = int>
class wrapper { /* ... */ };
template <WrappableType... T>
class wrapper { /* ... */ };

Concepts and constraints are discussed in Chapter 6, Concepts and Constraints. We will learn more about these kinds of parameters in that chapter. For now, let's look at the second kind of template parameters, non-type template parameters.

Non-type template parameters

Template arguments don't always have to represent types. They can also be compile-time expressions, such as constants, addresses of functions or objects with external linkage, or addresses of static class members. Parameters supplied with compile-time expressions are called non-type template parameters. This category of parameters can only have a structural type. The following are the structural types:

  • Integral types
  • Floating-point types, as of C++20
  • Enumerations
  • Pointer types (either to objects or functions)
  • Pointer to member types (either to member objects or member functions)
  • Lvalue reference types (either to objects or functions)
  • A literal class type that meets the following requirements:
    • All base classes are public and non-mutable.
    • All non-static data members are public and non-mutable.
    • The types of all base classes and non-static data members are also structural types or arrays thereof.

cv-qualified forms of these types can also be used for non-type template parameters. Non-type template parameters can be specified in different ways. The possible forms are shown in the following snippet:

template <int V>
class foo { /*...*/ };
template <int V = 42>
class foo { /*...*/ };
template <int... V>
class foo { /*...*/ };

In all these examples, the type of the non-type template parameters is int. The first and second examples are similar, except that in the second example a default value is used. The third example is significantly different because the parameter is actually a parameter pack. This will be discussed in the next chapter.

To understand non-type template parameters better, let's look at the following example, where we sketch a fixed-size array class, called buffer:

template <typename T, size_t S>
class buffer
{
   T data_[S];
public:
   constexpr T const * data() const { return data_; }
   constexpr T& operator[](size_t const index)
   {
      return data_[index];
   }
   constexpr T const & operator[](size_t const index) const
   {
      return data_[index];
   }
};

This buffer class holds an internal array of S elements of type T. Therefore, S needs to be a compile-type value. This class can be instantiated as follows:

buffer<int, 10> b1;
buffer<int, 2*5> b2;

These two definitions are equivalent, and both b1 and b2 are two buffers holding 10 integers. Moreover, they are of the same type, since 2*5 and 10 are two expressions evaluated to the same compile-time value. You can easily check this with the following statement:

static_assert(std::is_same_v<decltype(b1), decltype(b2)>);

This is not the case anymore, for the type of the b3 object is declared as follows:

buffer<int, 3*5> b3;

In this example, b3 is a buffer holding 15 integers, which is different from the buffer type from the previous example that held 10 integers. Conceptually, the compiler generates the following code:

template <typename T, size_t S>
class buffer
{
   T data_[S];
public:
   constexpr T* data() const { return data_; }
   constexpr T& operator[](size_t const index)
   {
      return data_[index];
   }
   constexpr T const & operator[](size_t const index) const
   {
      return data_[index];
   }
};

This is the code for the primary template but there are also a couple of specializations shown next:

template<>
class buffer<int, 10>
{
  int data_[10];
public: 
  constexpr int * data() const;
  constexpr int & operator[](const size_t index); 
  constexpr const int & operator[](
    const size_t index) const;
};
template<>
class buffer<int, 15>
{
  int data_[15]; 
public: 
  constexpr int * data() const;
  constexpr int & operator[](const size_t index);
  constexpr const int & operator[](
    const size_t index) const;
};

The concept of specialization, seen in this code sample, is detailed further on in this chapter, in the Understanding template specialization section. For the time being, you should notice the two different buffer types. Again, it's possible to verify that the types of b1 and b3 are different with the following statement:

static_assert(!std::is_same_v<decltype(b1), decltype(b3)>);

The use of structural types such as integer, floating-point, or enumeration types is encountered in practice more often than the rest. It's probably easier to understand their use and find useful examples for them. However, there are scenarios where pointers or references are used. In the following example, we will examine the use of a pointer to function parameter. Let's see the code first:

struct device
{
   virtual void output() = 0;
   virtual ~device() {}
};
template <void (*action)()>
struct smart_device : device
{
   void output() override
   {
      (*action)();
   }
};

In this snippet, device is a base class with a pure virtual function called output (and a virtual destructor). This is the base class for a class template called smart_device that implements the output virtual function by calling a function through a function pointer. This function pointer is passed an argument for the non-type template parameter of the class template. The following sample shows how it can be used:

void say_hello_in_english()
{
   std::cout << "Hello, world!
";
}
void say_hello_in_spanish()
{
   std::cout << "Hola mundo!
";
}
auto w1 =
   std::make_unique<smart_device<&say_hello_in_english>>();
w1->output();
auto w2 =
   std::make_unique<smart_device<&say_hello_in_spanish>>();
w2->output();

Here, w1 and w2 are two unique_ptr objects. Although, apparently, they point to objects of the same type, that is not true, because smart_device<&say_hello_in_english> and smart_device<&say_hello_in_spanish> are different types since they are instantiated with different values for the function pointer. This can be easily checked with the following statement:

static_assert(!std::is_same_v<decltype(w1), decltype(w2)>);

If we, on the other hand, change the auto specifier with std::unique_ptr<device>, as shown in the following snippet, then w1 and w2 are smart pointers to the base class device, and therefore have the same type:

std::unique_ptr<device> w1 = 
   std::make_unique<smart_device<&say_hello_in_english>>();
w1->output();
std::unique_ptr<device> w2 = 
   std::make_unique<smart_device<&say_hello_in_spanish>>();
w2->output();
static_assert(std::is_same_v<decltype(w1), decltype(w2)>);

Although this example uses a pointer to function, a similar example can be conceived for pointer to member functions. The previous example can be transformed to the following (still using the same base class device):

template <typename Command, void (Command::*action)()>
struct smart_device : device
{
   smart_device(Command& command) : cmd(command) {}
   void output() override
   {
      (cmd.*action)();
   }
private:
   Command& cmd;
};
struct hello_command
{
   void say_hello_in_english()
   {
      std::cout << "Hello, world!
";
   }
   void say_hello_in_spanish()
   {
      std::cout << "Hola mundo!
";
   }
};

These classes can be used as follows:

hello_command cmd;
auto w1 = std::make_unique<
   smart_device<hello_command, 
      &hello_command::say_hello_in_english>>(cmd);
w1->output();
auto w2 = std::make_unique<
   smart_device<hello_command, 
      &hello_command::say_hello_in_spanish>>(cmd);
w2->output();

In C++17, a new form of specifying non-type template parameters was introduced, using the auto specifier (including the auto* and auto& forms) or decltype(auto) instead of the name of the type. This allows the compiler to deduce the type of the parameter from the expression supplied as the argument. If the deduced type is not permitted for a non-type template parameter the compiler will generate an error. Let's see an example:

template <auto x>
struct foo
{ /* … */ };

This class template can be used as follows:

foo<42>   f1;  // foo<int>
foo<42.0> f2;  // foo<double> in C++20, error for older 
               // versions
foo<"42"> f3;  // error

In the first example, for f1, the compiler deduces the type of the argument as int. In the second example, for f2, the compiler deduces the type as double. However, this is only the case for C++20. In previous versions of the standard, this line would yield an error, since floating-point types were not permitted as arguments for non-type template parameters prior to C++20. The last line, however, produces an error because "42" is a string literal and string literals cannot be used as arguments for non-type template parameters.

The last example can be, however, worked around in C++20 by wrapping the literal string in a structural literal class. This class would store the characters of the string literal in a fixed-length array. This is exemplified in the following snippet:

template<size_t N>
struct string_literal
{
   constexpr string_literal(const char(&str)[N])
   {
      std::copy_n(str, N, value);
   }
   char value[N];
};

However, the foo class template shown previously needs to be modified to use string_literal explicitly and not the auto specifier:

template <string_literal x>
struct foo
{
};

With this is in place, the foo<"42"> f; declaration shown earlier will compile without any errors in C++20.

The auto specifier can also be used with a non-type template parameter pack. In this case, the type is deduced independently for each template argument. The types of the template arguments do not need to be the same. This is shown in the following snippet:

template<auto... x>
struct foo
{ /* ... */ };
foo<42, 42.0, false, 'x'> f;

In this example, the compiler deduces the types of the template arguments as int, double, bool, and char, respectively.

The third and last category of template parameters are template template parameters. We will look at them next.

Template template parameters

Although the name may sound a bit strange, it refers to a category of template parameters that are themselves templates. These can be specified similarly to type template parameters, with or without a name, with or without a default value, and as a parameter pack with or without a name. As of C++17, both the keywords class and typename can be used to introduce a template template parameter. Prior to this version, only the class keyword could be used.

To showcase the use of template template parameters, let's consider the following two class templates first:

template <typename T>
class simple_wrapper
{
public:
   T value;
};
template <typename T>
class fancy_wrapper
{
public:
   fancy_wrapper(T const v) :value(v)
   {
   }
   T const& get() const { return value; }
   template <typename U>
   U as() const
   {
      return static_cast<U>(value);
   }
private:
   T value;
};

The simple_wrapper class is a very simple class template that holds a value of the type template parameter T. On the other hand, fancy_wrapper is a more complex wrapper implementation that hides the wrapped value and exposes member functions for data access. Next, we implement a class template called wrapping_pair that contains two values of a wrapping type. This can be either simpler_wrapper, fancy_wrapper, or anything else that is similar:

template <typename T, typename U, 
          template<typename> typename W = fancy_wrapper>
class wrapping_pair
{
public:
   wrapping_pair(T const a, U const b) :
      item1(a), item2(b)
   {
   }
   W<T> item1;
   W<U> item2;
};   

The wrapping_pair class template has three parameters. The first two are type template parameters, named T and U. The third parameter is a template template parameter, called W, that has a default value, which is the fancy_wrapper type. We can use this class template as shown in the following snippet:

wrapping_pair<int, double> p1(42, 42.0);
std::cout << p1.item1.get() << ' '
          << p1.item2.get() << '
';
wrapping_pair<int, double, simple_wrapper> p2(42, 42.0);
std::cout << p2.item1.value << ' '
          << p2.item2.value << '
';

In this example, p1 is a wrapping_pair object that contains two values, an int and a double, each wrapped in a fancy_wrapper object. This is not explicitly specified but is the default value of the template template parameter. On the other hand, p2 is also a wrapping_pair object, also containing an int and a double, but these are wrapped by a simple_wrapper object, which is now specified explicitly in the template instantiation.

In this example, we have seen the use of a default template argument for a template parameter. This topic is explored in detail in the next section.

Default template arguments

Default template arguments are specified similarly to default function arguments, in the parameter list after the equal sign. The following rules apply to default template arguments:

  • They can be used with any kind of template parameters with the exception of parameter packs.
  • If a default value is specified for a template parameter of a class template, variable template, or type alias, then all subsequent template parameters must also have a default value. The exception is the last parameter if it is a template parameter pack.
  • If a default value is specified for a template parameter in a function template, then subsequent template parameters are not restricted to also have a default value.
  • In a function template, a parameter pack may be followed by more type parameters only if they have default arguments or their value can be deduced by the compiler from the function arguments.
  • They are not allowed in declarations of friend class templates.
  • They are allowed in the declaration of a friend function template only if the declaration is also a definition and there is no other declaration of the function in the same translation unit.
  • They are not allowed in the declaration or definition of an explicit specialization of a function template or member function template.

The following snippet shows examples for using default template arguments:

template <typename T = int>
class foo { /*...*/ };
template <typename T = int, typename U = double>
class bar { /*...*/ };

As mentioned previously, a template parameter with a default argument cannot be followed by parameters without a default argument when declaring a class template but this restriction does not apply to function templates. This is shown in the next snippet:

template <typename T = int, typename U>
class bar { };   // error
template <typename T = int, typename U>
void func() {}   // OK

A template may have multiple declarations (but only one definition). The default template arguments from all the declarations and the definition are merged (the same way they are merged for default function arguments). Let's look at an example to understand how it works:

template <typename T, typename U = double>
struct foo;
template <typename T = int, typename U>
struct foo;
template <typename T, typename U>
struct foo
{
   T a;
   U b;
};

This is semantically equivalent to the following definition:

template <typename T = int, typename U = double>
struct foo
{
   T a;
   U b;
};

However, these multiple declarations with different default template arguments cannot be provided in any order. The rules mentioned earlier still apply. Therefore, a declaration of a class template where the first parameter has a default argument and the ensuing parameters do not have one is illegal:

template <typename T = int, typename U>
struct foo;  // error, U does not have a default argument
template <typename T, typename U = double>
struct foo;

Another restriction on default template arguments is that the same template parameter cannot be given multiple defaults in the same scope. Therefore, the next example will produce an error:

template <typename T = int>
struct foo;
template <typename T = int> // error redefinition
                            // of default parameter
struct foo {};

When a default template argument uses names from a class, the member access restrictions are checked at the declaration, not at the instantiation of the template:

template <typename T>
struct foo
{
protected:
   using value_type = T;
};
template <typename T, typename U = typename T::value_type>
struct bar
{
   using value_type = U;
};
bar<foo<int>> x;

When the x variable is defined, the bar class template is instantiated, but the foo::value_type typedef is protected and therefore cannot be used outside of foo. The result is a compiler error at the declaration of the bar class template.

With these mentions, we wrap up the topic of template parameters. The next one we will explore in the following section is template instantiation, which is the creation of a new definition of a function, class, or variable from a template definition and a set of template arguments.

Understanding template instantiation

As mentioned before, templates are only blueprints from which the compiler creates actual code when it encounters their use. The act of creating a definition for a function, a class, or a variable from the template declaration is called template instantiation. This can be either explicit, when you tell the compiler when it should generate a definition, or implicit, when the compiler generates a new definition as needed. We will look at these two forms in detail in the next sections.

Implicit instantiation

Implicit instantiation occurs when the compiler generates definitions based on the use of templates and when no explicit instantiation is present. Implicitly instantiated templates are defined in the same namespace as the template. However, the way compilers create definitions from templates may differ. This is something we will see in the following example. Let's consider this code:

template <typename T>
struct foo
{
  void f() {}
};
int main()
{
  foo<int> x;
}

Here, we have a class template called foo with a member function f. In main, we define a variable of the type foo<int> but do not use any of its members. Because it encounters this use of foo, the compiler implicitly defines a specialization of foo for the int type. If you use cppinsights.io, which runs in Clang, you will see the following code:

template<>
struct foo<int>
{
  inline void f();
};

Because the function f is not invoked in our code, it is only declared but not defined. Should we add a call f in main, the specialization would change as follows:

template<>
struct foo<int>
{
  inline void f() { }
};

However, if we add one more function, g, with the following implementation that contains an error, we will get different behaviors with different compilers:

template <typename T>
struct foo
{
  void f() {}
  void g() {int a = "42";}
};
int main()
{
  foo<int> x;
  x.f();
}

The body of g contains an error (you could also use a static_assert(false) statement as an alternative). This code compiles without any problem with VC++, but fails with Clang and GCC. This is because VC++ ignores the parts of the template that are not used, provided that the code is syntactically correct, but the others perform semantic validation before proceeding with template instantiation.

For function templates, implicit instantiation occurs when the user code refers to a function in a context that requires its definition to exist. For class templates, implicit instantiation occurs when the user code refers to a template in a context when a complete type is required or when the completeness of the type affects the code. The typical example of such a context is when an object of such a type is constructed. However, this is not the case when declaring pointers to a class template. To understand how this works, let's consider the following example:

template <typename T>
struct foo
{
  void f() {}
  void g() {}
};
int main()
{
  foo<int>* p;
  foo<int> x;
  foo<double>* q;
}

In this snippet, we use the same foo class template from the previous examples, and we declare several variables: p which is a pointer to foo<int>, x which is a foo<int>, and q which is a pointer to foo<double>. The compiler is required to instantiate only foo<int> at this point because of the declaration of x. Now, let's consider some invocations of the member functions f and g as follows:

int main()
{
  foo<int>* p;
  foo<int> x;
  foo<double>* q;
  x.f();
  q->g();
}

With these changes, the compiler is required to instantiate the following:

  • foo<int> when the x variable is declared
  • foo<int>::f() when the x.f() call occurs
  • foo<double> and foo<double>::g() when the q->g() call occurs.

On the other hand, the compiler is not required to instantiate foo<int> when the p pointer is declared nor foo<double> when the q pointer is declared. However, the compiler does need to implicitly instantiate a class template specialization when it is involved in pointer conversion. This is shown in the following example:

template <typename T>
struct control
{};
template <typename T>
struct button : public control<T>
{};
void show(button<int>* ptr)
{
   control<int>* c = ptr;
}

In the function show, a conversion between button<int>* and control<int>* takes place. Therefore, at this point, the compiler must instantiate button<int>.

When a class template contains static members, those members are not implicitly instantiated when the compiler implicitly instantiates the class template but only when the compiler needs their definition. On the other hand, every specialization of a class template has its own copy of static members as exemplified in the following snippet:

template <typename T>
struct foo
{
   static T data;
};
template <typename T> T foo<T>::data = 0;
int main()
{
   foo<int> a;
   foo<double> b;
   foo<double> c;
   std::cout << a.data << '
'; // 0
   std::cout << b.data << '
'; // 0
   std::cout << c.data << '
'; // 0
   b.data = 42;
   std::cout << a.data << '
'; // 0
   std::cout << b.data << '
'; // 42
   std::cout << c.data << '
'; // 42
}

The class template foo has a static member variable called data that is initialized after the definition of foo. In the main function, we declare the variable a as an object of foo<int> and b and c as objects of foo<double>. Initially, all of them have the member field data initialized with 0. However, the variables b and c share the same copy of data. Therefore, after the assignment b.data = 42, a.data is still 0, but both b.data and c.data are 42.

Having learned how implicit instantiation works, it is time to move forward and understand the other form of template instantiation, which is explicit instantiation.

Explicit instantiation

As a user, you can explicitly tell the compiler to instantiate a class template or a function template. This is called explicit instantiation and it has two forms: explicit instantiation definition and explicit instantiation declaration. We will discuss them in this order.

Explicit instantiation definition

An explicit instantiation definition may appear anywhere in a program but after the definition of the template it refers to. The syntax for explicit template instantiation definitions takes the following forms:

  • The syntax for class templates is as follows:

    template class-key template-name <argument-list>

  • The syntax for function templates is as follows:

    template return-type name<argument-list>(parameter-list);

    template return-type name(parameter-list);

As you can see, in all cases, the explicit instantiation definition is introduced with the template keyword but not followed by any parameter list. For class templates, the class-key can be any of the class, struct, or union keywords. For both class and function templates, an explicit instantiation definition with a given argument list can only appear once in the entire program.

We will look at some examples to understand how this works. Here is the first example:

namespace ns
{
   template <typename T>
   struct wrapper
   {
      T value;
   };
   template struct wrapper<int>;       // [1]
}
template struct ns::wrapper<double>;   // [2]
int main() {}

In this snippet, wrapper<T> is a class template defined in the ns namespace. The statements marked with [1] and [2] in the code are both representing an explicit instantiation definition, for wrapper<int> and wrapper<double> respectively. An explicit instantiation definition can only appear in the same namespace as the template it refers to (as in [1]) to or it must be fully qualified (as in [2]). We can write similar explicit template definitions for a function template:

namespace ns
{
   template <typename T>
   T add(T const a, T const b)
   {
      return a + b;
   }
   template int add(int, int);           // [1]
}
template double ns::add(double, double); // [2]
int main() { }

This second example has a striking resemblance to the first. Both [1] and [2] represent explicit template definitions for add<int>() and add<double>().

If the explicit instantiation definition is not in the same namespace as the template, the name must be fully qualified. The use of a using statement does not make the name visible in the current namespace. This is shown in the following example:

namespace ns
{
   template <typename T>
   struct wrapper { T value; };
}
using namespace ns;
template struct wrapper<double>;   // error

The last line in this example generates a compile error because wrapper is an unknown name and must be qualified with the namespace name, as in ns::wrapper.

When class members are used for return types or parameter types, member access specification is ignored in explicit instantiation definitions. An example is shown in the following snippet:

template <typename T>
class foo
{
   struct bar {};
   T f(bar const arg)
   {
      return {};
   }
};
template int foo<int>::f(foo<int>::bar);

Both the class X<T>::bar and the function foo<T>::f() are private to the foo<T> class, but they can be used in the explicit instantiation definition shown on the last line.

Having seen what explicit instantiation definition is and how it works, the question that arises is when is it useful. Why would you tell the compiler to generate instantiation from a template? The answer is that it helps distribute libraries, reduce build times, and executable sizes. If you are building a library that you want to distribute as a .lib file and that library uses templates, the template definitions that are not instantiated are not put into the library. But that leads to increased build times of your user code every time you use the library. By forcing instantiations of templates in the library, those definitions are put into the object files and the .lib file you are distributing. As a result, your user code only needs to be linked to those available functions in the library file. This is what the Microsoft MSVC CRT libraries do for all the stream, locale, and string classes. The libstdc++ library does the same for string classes and others.

A problem that can arise with template instantiations is that you can end up with multiple definitions, one per translation unit. If the same header that contains a template is included in multiple translation units (.cpp files) and the same template instantiation is used (let's say wrapper<int> from our previous examples), then identical copies of these instantiations are put in each translation unit. This leads to increased object sizes. The problem can be solved with the help of explicit instantiation declarations, which we will look at next.

Explicit instantiation declaration

An explicit instantiation declaration (available with C++11) is the way you can tell the compiler that the definition of a template instantiation is found in a different translation unit and that a new definition should not be generated. The syntax is the same as for explicit instantiation definitions except that the keyword extern is used in front of the declaration:

  • The syntax for class templates is as follows:

    extern template class-key template-name <argument-list>

  • The syntax for function templates is as follows:

    extern template return-type name<argument-list>(parameter-list);

    extern template return-type name(parameter-list);

If you provide an explicit instantiation declaration but no instantiation definition exists in any translation unit of the program, then the result is a compiler warning and a linker error. The technique is to declare an explicit template instantiation in one source file and explicit template declarations in the remaining ones. This will reduce both compilation times and object file sizes.

Let's look at the following example:

// wrapper.h
template <typename T>
struct wrapper
{
   T data;
}; 
extern template wrapper<int>;   // [1]
// source1.cpp
#include "wrapper.h"
#include <iostream>
template wrapper<int>;          // [2]
void f()
{
   ext::wrapper<int> a{ 42 };
   std::cout << a.data << '
';
}
// source2.cpp
#include "wrapper.h"
#include <iostream>
void g()
{
   wrapper<int> a{ 100 };
   std::cout << a.data << '
';
}
// main.cpp
#include "wrapper.h"
int main()
{
   wrapper<int> a{ 0 };
}

In this example, we can see the following:

  • The wrapper.h header contains a class template called wrapper<T>. On the line marked with [1] there is an explicit instantiation declaration for wrapper<int> that tells the compiler not to generate definitions for this instantiation when a source file (translation unit) including this header is compiled.
  • The source1.cpp file includes wrapper.h and on the line marked with [2] contains an explicit instantiation definition for wrapper<int>. This is the only definition for this instantiation within the entire program.
  • The source files source2.cpp and main.cpp are both using wrapper<int> but without any explicit instantiation definition or declaration. That is because the explicit declaration from wrapper.h is visible when the header is included in each of these files.

Alternatively, the explicit instantiation declaration could be taken away from the header file but then it must be added to each source file that includes the header and that is likely to be forgotten.

When you do explicit template declarations, keep in mind that a class member function that is defined within the body of the class is always considered inline and therefore it will always be instantiated. Therefore, you can only use the extern keyword for member functions that are defined outside of the class body.

Now that we have looked at what template instantiation is, we will continue with another important topic, template specialization, which is the term used for the definition created from a template instantiation to handle a specific set of template arguments.

Understanding template specialization

A template specialization is the definition created from a template instantiation. The template that is being specialized is called the primary template. You can provide an explicit specialized definition for a given set of template arguments, therefore overwriting the implicit code the compiler would generate instead. This is the technique that powers features such as type traits and conditional compilation, which are metaprogramming concepts we will explore in Chapter 5, Type Traits and Conditional Compilation.

There are two forms of template specialization: explicit (full) specialization and partial specialization. We will look in detail at these two in the following sections.

Explicit specialization

Explicit specialization (also called full specialization) occurs when you provide a definition for a template instantiation with the full set of template arguments. The following can be fully specialized:

  • Function templates
  • Class templates
  • Variable templates (as of C++14)
  • Member functions, classes, and enumerations of a class template
  • Member function templates and class templates of a class or class template
  • Static data members of a class template

Let's start by looking at the following example:

template <typename T>
struct is_floating_point
{
   constexpr static bool value = false;
};
template <>
struct is_floating_point<float>
{
   constexpr static bool value = true;
};
template <>
struct is_floating_point<double>
{
   constexpr static bool value = true;
};
template <>
struct is_floating_point<long double>
{
   constexpr static bool value = true;
};

In this code snippet, is_floating_point is the primary template. It contains a constexpr static Boolean data member called value that is initialized with the false value. Then, we have three full specializations of this primary template, for the float, double, and long double types. These new definitions change the way value is being initialized using true instead of false. As a result, we can use this template to write code as follows:

std::cout << is_floating_point<int>::value         << '
';
std::cout << is_floating_point<float>::value       << '
';
std::cout << is_floating_point<double>::value      << '
';
std::cout << is_floating_point<long double>::value << '
';
std::cout << is_floating_point<std::string>::value << '
';

The first and last lines print 0 (for false); the other lines print 1 (for true). This example is a demonstration of how type traits work. In fact, the standard library contains a class template called is_floating_point in the std namespace, defined in the <type_traits> header. We will learn more about this topic in Chapter 5, Type Traits and Conditional Compilation.

As you can see in this example, static class members can be fully specialized. However, each specialization has its own copy of any static members, which is demonstrated with the following example:

template <typename T>
struct foo
{
   static T value;
};
template <typename T> T foo<T>::value = 0;
template <> int foo<int>::value = 42;
foo<double> a, b;  // a.value=0, b.value=0
foo<int> c;        // c.value=42
a.value = 100;     // a.value=100, b.value=100, c.value=42

Here, foo<T> is a class template with a single static member, called value. This is initialized with 0 for the primary template and with 42 for the int specialization. After declaring the variables a, b, and c, a.value is 0 and b.value is 0 while c.value is 42. However, after assigning the value 100 to a.value, b.value is also 100, while c.value remains 42.

Explicit specialization must appear after the primary template declaration. It does not require a definition of the primary template to be available prior to the explicit specialization. The following code is therefore valid:

template <typename T>
struct is_floating_point;
template <>
struct is_floating_point<float>
{
   constexpr static bool value = true;
};
template <typename T>
struct is_floating_point
{
   constexpr static bool value = false;
};

Template specializations can also be only declared without being defined. Such a template specialization can be used like any other incomplete type. You can see an example of this here:

template <typename>
struct foo {};    // primary template
template <>
struct foo<int>;  // explicit specialization declaration
foo<double> a; // OK
foo<int>* b;   // OK
foo<int> c;    // error, foo<int> incomplete type

In this example, foo<T> is the primary template for which a declaration of an explicit specialization for the int type exists. This makes it possible to use foo<double> and foo<int>* (declaring pointers to partial types is supported). However, at the point of declaring the c variable, the complete type foo<int> is not available, since a definition of the full specialization for int is missing. This generates a compiler error.

When specializing a function template, if the compiler can deduce a template argument from the type of the function arguments, then that template argument is optional. This is demonstrated by the following example:

template <typename T>
struct foo {};
template <typename T>
void func(foo<T>) 
{
   std::cout << "primary template
";
}
template<>
void func(foo<int>) 
{
   std::cout << "int specialization
";
}

The syntax for the full specialization for int of the func function template should be template<> func<int>(foo<int>). However, the compiler is able to deduce the actual type that T represents from the function argument. Therefore, we don't have to specify it when defining the specialization.

On the other hand, declarations or definitions of function templates and member function templates are not allowed to contain default function arguments. Therefore, in the following example, the compiler will issue an error:

template <typename T>
void func(T a)
{
   std::cout << "primary template
";
}
template <>
void func(int a = 0) // error: default argument not allowed
{
   std::cout << "int specialization
";
}

In all these examples, the templates had a single template argument. However, in practice, many templates have multiple arguments. Explicit specialization requires a definition with the full set of arguments being specified. This is demonstrated with the following snippet:

template <typename T, typename U>
void func(T a, U b)
{
   std::cout << "primary template
";
}
template <>
void func(int a, int b)
{
   std::cout << "int-int specialization
";
}
template <>
void func(int a, double b)
{
   std::cout << "int-double specialization
";
}
func(1, 2);      // int-int specialization
func(1, 2.0);    // int-double specialization
func(1.0, 2.0);  // primary template

With these covered, we can move forward and look into partial specialization, which is basically a generalization of explicit (full) specialization.

Partial specialization

Partial specialization occurs when you specialize a primary template but only specify some of the template arguments. This means a partial specialization has both a template parameter list (which follows the template keyword) and a template argument list (which follows the template name). However, only classes can be partially specialized.

Let's explore the following example to understand how this works:

template <typename T, int S>
struct collection
{
   void operator()()
   { std::cout << "primary template
"; }
};
template <typename T>
struct collection<T, 10>
{
   void operator()()
   { std::cout << "partial specialization <T, 10>
"; }
};
template <int S>
struct collection<int, S>
{ 
   void operator()()
   { std::cout << "partial specialization <int, S>
"; }
};
template <typename T, int S>
struct collection<T*, S>
{ 
   void operator()()
   { std::cout << "partial specialization <T*, S>
"; }
};

We have a primary template called collection that has two template arguments (a type template argument and a non-type template argument) and we have three partial specializations, as follows:

  • A specialization for the non-type template argument S with the value 10
  • A specialization for the int type
  • A specialization for the pointer type T*

These templates can be used as shown in the following snippet:

collection<char, 42> a;  // primary template
collection<int,  42> b;  // partial specialization <int, S>
collection<char, 10> c;  // partial specialization <T, 10>
collection<int*, 20> d;  // partial specialization <T*, S>

As specified in the comments, a is instantiated from the primary template, b from the partial specialization for int (collection<int, S>), c from the partial specialization for 10 (collection<T, 10>), and d from the partial specialization for pointers (collection<T*, S>). However, some combinations are not possible because they are ambiguous and the compiler cannot select which template instantiation to use. Here are a couple of examples:

collection<int,   10> e; // error: collection<T,10> or 
                         //        collection<int,S>
collection<char*, 10> f; // error: collection<T,10> or 
                         //        collection<T*,S>

In the first case, both collection<T, 10> and collection<int, S> partial specializations match the type collection<int, 10>, while in the second case it can be either collection<T, 10> or collection<T*, S>.

When defining specializations of a primary template, you need to keep in mind the following:

  • Parameters in the template parameters list of the partial specialization cannot have default values.
  • The template parameters list implies an order of the arguments in the template arguments list, which is featured only in a partial specialization. This template arguments list of a partial specialization cannot be the same as the one implied by the template parameters list.
  • In the template argument list, you can only use identifiers for non-type template parameters. Expressions are not allowed in this context. This is demonstrated with the following example:

    template <int A, int B> struct foo {};

    template <int A> struct foo<A, A> {};     // OK

    template <int A> struct foo<A, A + 1> {}; // error

When a class template has partial specializations, the compiler must decide what is the best match to generate a definition from. For this purpose, it matches the template arguments of the template specialization with the template argument list of the primary template and partial specializations. Depending on the result of this matching process, the compiler does the following:

  • If no match is found, a definition is generated from the primary template.
  • If a single partial specialization is found, a definition is generated from that specialization.
  • If more than a single partial specialization is found, then a definition is generated from the most specialized partial specialization but only if it is unique. Otherwise, the compiler generates an error (as we have seen previously). A template A is considered more specialized than a template B if it accepts a subset of the types that B accepts, but not the other way around.

However, partial specializations are not found by name lookup and are considered only if the primary template is found by name lookup.

To understand how partial specialization is useful, let's take a look at a real-world example.

In this example, we want to create a function that formats the content of an array in a nice way and outputs it to a stream. The content of a formatted array should look like [1,2,3,4,5]. However, for arrays of char elements, the elements should not be separated by a comma but instead displayed as a string within square brackets, such as [demo]. For this purpose, we will consider the use of the std::array class. The following implementation formats the content of the array with delimiters between the elements:

template <typename T, size_t S>
std::ostream& pretty_print(std::ostream& os, 
                           std::array<T, S> const& arr)
{
   os << '[';
   if (S > 0)
   {
      size_t i = 0;
      for (; i < S - 1; ++i)
         os << arr[i] << ',';
      os << arr[S-1];
   }
   os << ']';
   return os;
}
std::array<int, 9> arr {1, 1, 2, 3, 5, 8, 13, 21};
pretty_print(std::cout, arr);  // [1,1,2,3,5,8,13,21]
std::array<char, 9> str;
std::strcpy(str.data(), "template");
pretty_print(std::cout, str);  // [t,e,m,p,l,a,t,e]

In this snippet, pretty_print is a function template with two template parameters, matching the template parameters of the std::array class. When called with the arr array as an argument, it prints [1,1,2,3,5,8,13,21]. When called with the str array as an argument, it prints [t,e,m,p,l,a,t,e]. However, our intention is to print [template] in this latter case. For this, we need another implementation, which is specialized for the char type:

template <size_t S>
std::ostream& pretty_print(std::ostream& os, 
                           std::array<char, S> const& arr)
{
   os << '[';
   for (auto const& e : arr)
      os << e;
   os << ']';
   return os;
}
std::array<char, 9> str;
std::strcpy(str.data(), "template");
pretty_print(std::cout, str);  // [template]

In this second implementation, pretty_print is a function template with a single template parameter, which is a non-type template parameter indicating the size of the array. The type template parameter is explicitly specified as char, in std::array<char, S>. This time, the call to pretty_print with the str array prints [template] to the console.

What is key to understand here is that it's not the pretty_print function template that is partially specialized but the std::array class template. Function templates cannot be specialized and what we have here are overloaded functions. However, std::array<char,S> is a specialization of the primary class template std::array<T, S>.

All the examples we have seen in this chapter were either function templates or class templates. However, variables can also be templates and this will be the topic of the next section.

Defining variable templates

Variable templates were introduced in C++14 and allow us to define variables that are templates either at namespace scope, in which case they represent a family of global variables, or at class scope, in which case they represent static data members.

A variable template is declared at a namespace scope as shown in the following code snippet. This is a typical example that you can find in the literature, but we can use it to elaborate on the benefits of variable templates:

template<class T>
constexpr T PI = T(3.1415926535897932385L);

The syntax is similar to declaring a variable (or data member) but combined with the syntax for declaring templates.

The question that arises is how variable templates are actually helpful. To answer this, let's build up an example to demonstrate the point. Let's consider we want to write a function template that, given the radius of a sphere, returns its volume. The volume of a sphere is 4πr^3 / 3. Therefore, a possible implementation is as follows:

constexpr double PI = 3.1415926535897932385L;
template <typename T>
T sphere_volume(T const r)
{
   return 4 * PI * r * r * r / 3;
}

In this example, PI is defined as a compile-time constant of the double type. This will generate a compiler warning if we use float, for instance, for the type template parameter T:

float v1 = sphere_volume(42.0f); // warning
double v2 = sphere_volume(42.0); // OK

A potential solution to this problem is to make PI a static data member of a template class with its type determined by the type template parameter. This implementation can look as follows:

template <typename T>
struct PI
{
   static const T value;
};
template <typename T> 
const T PI<T>::value = T(3.1415926535897932385L);
template <typename T>
T sphere_volume(T const r)
{
   return 4 * PI<T>::value * r * r * r / 3;
}

This works, although the use of PI<T>::value is not ideal. It would be nicer if we could simply write PI<T>. This is exactly what the variable template PI shown at the beginning of the section allows us to do. Here it is again, with the complete solution:

template<class T>
constexpr T PI = T(3.1415926535897932385L);
template <typename T>
T sphere_volume(T const r)
{
   return 4 * PI<T> * r * r * r / 3;
}

The next example shows yet another possible use case and also demonstrates the explicit specialization of variable templates:

template<typename T> 
constexpr T SEPARATOR = '
';
template<> 
constexpr wchar_t SEPARATOR<wchar_t> = L'
';
template <typename T>
std::basic_ostream<T>& show_parts(
   std::basic_ostream<T>& s, 
   std::basic_string_view<T> const& str)
{
   using size_type = 
      typename std::basic_string_view<T>::size_type;
   size_type start = 0;
   size_type end;
   do
   {
      end = str.find(SEPARATOR<T>, start);
      s << '[' << str.substr(start, end - start) << ']' 
        << SEPARATOR<T>;
      start = end+1;
   } while (end != std::string::npos);
   return s;
}
show_parts<char>(std::cout, "one
two
three");
show_parts<wchar_t>(std::wcout, L"one line");

In this example, we have a function template called show_parts that processes an input string after splitting it into parts delimited by a separator. The separator is a variable template defined at (global) namespace scope and is explicitly specialized for the wchar_t type.

As previously mentioned, variable templates can be members of classes. In this case, they represent static data members and need to be declared using the static keyword. The following example demonstrates this:

struct math_constants
{
   template<class T>
   static constexpr T PI = T(3.1415926535897932385L);
};
template <typename T>
T sphere_volume(T const r)
{
   return 4 * math_constants::PI<T> *r * r * r / 3;
}

You can declare a variable template in a class and then provide its definition outside the class. Notice that in this case, the variable template must be declared with static const and not static constexpr, since the latter one requires in-class initialization:

struct math_constants
{
   template<class T>
   static const T PI;
};
template<class T>
const T math_constants::PI = T(3.1415926535897932385L);

Variable templates are used to simplify the use of type traits. The Explicit specialization section contained an example for a type trait called is_floating_point. Here is, again, the primary template:

template <typename T>
struct is_floating_point
{
   constexpr static bool value = false;
};

There were several explicit specializations that I will not list here again. However, this type trait can be used as follows:

std::cout << is_floating_point<float>::value << '
';

The use of is_floating_point<float>::value is rather cumbersome, but can be avoided with the help of a variable template that can be defined as follows:

template <typename T>
inline constexpr bool is_floating_point_v = 
   is_floating_point<T>::value;

This is_floating_point_v variable template helps write code that is arguably simpler and easier to read. The following snippet is the form I prefer over the verbose variant with ::value:

std::cout << is_floating_point_v<float> << '
';

The standard library defines a series of variable templates suffixed with _v for ::value, just as in our example (such as std::is_floating_point_v or std::is_same_v). We will discuss this topic in more detail in Chapter 5, Type Traits and Conditional Compilation.

Variable templates are instantiated similarly to function templates and class templates. This happens either with an explicit instantiation or explicit specialization, or implicitly by the compiler. The compiler generates a definition when the variable template is used in a context where a variable definition must exist, or the variable is needed for constant evaluation of an expression.

After this, we move to the topic of alias templates, which allow us to define aliases for class templates.

Defining alias templates

In C++, an alias is a name used to refer to a type that has been previously defined, whether a built-in type or a user-defined type. The primary purpose of aliases is to give shorter names to types that have a long name or provide semantically meaningful names for some types. This can be done either with a typedef declaration or with a using declaration (the latter was introduced in C++11). Here are several examples using typedef:

typedef int index_t;
typedef std::vector<
           std::pair<int, std::string>> NameValueList;
typedef int (*fn_ptr)(int, char);
template <typename T>
struct foo
{
   typedef T value_type;
};

In this example, index_t is an alias for int, NameValueList is an alias for std::vector<std::pair<int, std::string>>, while fn_ptr is an alias for the type of a pointer to a function that returns an int and has two parameters of type int and char. Lastly, foo::value_type is an alias for the type template T.

Since C++11, these type aliases can be created with the help of using declarations, which look as follows:

using index_t = int;
using NameValueList = 
   std::vector<std::pair<int, std::string>>;
using fn_ptr = int(*)(int, char);
template <typename T>
struct foo
{
   using value_type = T;
};

Using declarations are now preferred over typedef declarations because they are simpler to use and are also more natural to read (from left to right). However, they have an important advantage over typedefs as they allow us to create aliases for templates. An alias template is a name that refers not to a type but a family of types. Remember, a template is not a class, function, or variable but a blueprint that allows the creation of a family of types, functions, or variables.

To understand how alias templates work, let's consider the following example:

template <typename T>
using customer_addresses_t = 
   std::map<int, std::vector<T>>;            // [1]
struct delivery_address_t {};
struct invoice_address_t {};
using customer_delivery_addresses_t =
   customer_addresses_t<delivery_address_t>; // [2]
using customer_invoice_addresses_t =
   customer_addresses_t<invoice_address_t>;  // [3]

The declaration on line [1] introduces the alias template customer_addresses_t. It's an alias for a map type where the key type is int and the value type is std::vector<T>. Since std::vector<T> is not a type, but a family of types, customer_addresses_t<T> defines a family of types. The using declarations at [2] and [3] introduce two type aliases, customer_delivery_addresses_t and customer_invoice_addresses_t, from the aforementioned family of types.

Alias templates can appear at namespace or class scope just like any template declaration. On the other hand, they can neither be fully nor partially specialized. However, there are ways to overcome this limitation. A solution is to create a class template with a type alias member and specialize the class. Then you can create an alias template that refers to the type alias member. Let's demonstrate this with the help of an example.

Although the following is not valid C++ code, it represents the end goal I want to achieve, had the specialization of alias templates been possible:

template <typename T, size_t S>
using list_t = std::vector<T>;
template <typename T>
using list_t<T, 1> = T;

In this example, list_t is an alias template for std::vector<T> provided the size of the collection is greater than 1. However, if there is a single element, then list_t should be an alias for the type template parameter T. The way this can be actually achieved is shown in the following snippet:

template <typename T, size_t S>
struct list
{
   using type = std::vector<T>;
};
template <typename T>
struct list<T, 1>
{
   using type = T;
};
template <typename T, size_t S>
using list_t = typename list<T, S>::type;

In this example, list<T,S> is a class template that has a member type alias called T. In the primary template, this is an alias for std::vector<T>. In the partial specialization list<T,1> it's an alias for T. Then, list_t is defined as an alias template for list<T, S>::type. The following asserts prove this mechanism works:

static_assert(std::is_same_v<list_t<int, 1>, int>);
static_assert(std::is_same_v<list_t<int, 2>, std::vector<int>>);

Before we end this chapter, there is one more topic that needs to be addressed: generic lambdas and their C++20 improvement, lambda templates.

Exploring generic lambdas and lambda templates

Lambdas, which are formally called lambda expressions, are a simplified way to define function objects in the place where they are needed. This typically includes predicates or comparison functions passed to algorithms. Although we will not discuss lambda expressions in general, let's take a look at the following examples:

int arr[] = { 1,6,3,8,4,2,9 };
std::sort(
   std::begin(arr), std::end(arr),
   [](int const a, int const b) {return a > b; });
int pivot = 5;
auto count = std::count_if(
   std::begin(arr), std::end(arr),
   [pivot](int const a) {return a > pivot; });

Lambda expressions are syntactic sugar, a simplified way of defining anonymous function objects. When encountering a lambda expression, the compiler generates a class with a function-call operator. For the previous example, these could look as follows:

struct __lambda_1
{
   inline bool operator()(const int a, const int b) const
   {
      return a > b;
   }
};
struct __lambda_2
{
   __lambda_2(int & _pivot) : pivot{_pivot}
   {} 
   inline bool operator()(const int a) const
   {
      return a > pivot;
   }
private: 
   int pivot;
};

The names chosen here are arbitrary and each compiler will generate different names. Also, the implementation details may differ and the ones seen here are the bare minimum a compiler is supposed to generate. Notice that the difference between the first lambda and the second is that the latter contains state that it captures by value.

Lambda expressions, which were introduced in C++11, have received several updates in later versions of the standard. There are notably two, which will be discussed in this chapter:

  • Generic lambdas, introduced in C++14, allow us to use the auto specifier instead of explicitly specifying types. This transforms the generated function object into one with a template function-call operator.
  • Template lambdas, introduced in C++20, allow us to use the template syntax to explicitly specify the shape of the templatized function-call operator.

To understand the difference between these and how generic and template lambdas are helpful, let's explore the following examples:

auto l1 = [](int a) {return a + a; };  // C++11, regular 
                                       // lambda
auto l2 = [](auto a) {return a + a; }; // C++14, generic 
                                       // lambda
auto l3 = []<typename T>(T a) 
          { return a + a; };   // C++20, template lambda
auto v1 = l1(42);                      // OK
auto v2 = l1(42.0);                    // warning
auto v3 = l1(std::string{ "42" });     // error
auto v5 = l2(42);                      // OK
auto v6 = l2(42.0);                    // OK
auto v7 = l2(std::string{"42"});       // OK
auto v8 = l3(42);                      // OK
auto v9 = l3(42.0);                    // OK
auto v10 = l3(std::string{ "42" });    // OK

Here, we have three different lambdas: l1 is a regular lambda, l2 is a generic lambda, as at least one of the parameters is defined with the auto specifier, and l3 is a template lambda, defined with the template syntax but without the use of the template keyword.

We can invoke l1 with an integer; we can also invoke it with a double, but this time the compiler will produce a warning about the possible loss of data. However, trying to invoke it with a string argument will produce a compile error, because std::string cannot be converted to int. On the other hand, l2 is a generic lambda. The compiler proceeds to instantiate specializations of it for all the types of the arguments it's invoked with, in this example int, double, and std::string. The following snippet shows how the generated function object may look, at least conceptually:

struct __lambda_3
{
   template<typename T1>
   inline auto operator()(T1 a) const
   {
     return a + a;
   }
   template<>
   inline int operator()(int a) const
   {
     return a + a;
   }
   template<>
   inline double operator()(double a) const
   {
     return a + a;
   }
   template<>
   inline std::string operator()(std::string a) const
   {
     return std::operator+(a, a);
   }
};

You can see here the primary template for the function-call operator, as well as the three specializations that we mentioned. Not surprisingly, the compiler will generate the same code for the third lambda expression, l3, which is a template lambda, only available in C++20. The question that arises from this is how are generic lambdas and lambda templates different? To answer this question, let's modify the previous example a bit:

auto l1 = [](int a, int b) {return a + b; };
auto l2 = [](auto a, auto b) {return a + b; };
auto l3 = []<typename T, typename U>(T a, U b) 
          { return a + b; };
auto v1 = l1(42, 1);                    // OK
auto v2 = l1(42.0, 1.0);                // warning
auto v3 = l1(std::string{ "42" }, '1'); // error
auto v4 = l2(42, 1);                    // OK
auto v5 = l2(42.0, 1);                  // OK
auto v6 = l2(std::string{ "42" }, '1'); // OK
auto v7 = l2(std::string{ "42" }, std::string{ "1" }); // OK 
auto v8 = l3(42, 1);                    // OK
auto v9 = l3(42.0, 1);                  // OK
auto v10 = l3(std::string{ "42" }, '1'); // OK
auto v11 = l3(std::string{ "42" }, std::string{ "42" }); // OK 

The new lambda expressions take two parameters. Again, we can call l1 with two integers or an int and a double (although this again generates a warning) but we can't call it with a string and char. However, we can do all these with the generic lambda l2 and the lambda template l3. The code the compiler generates is identical for l2 and l3 and looks, semantically, as follows:

struct __lambda_4
{
   template<typename T1, typename T2>
   inline auto operator()(T1 a, T2 b) const
   {
     return a + b;
   }
   template<>
   inline int operator()(int a, int b) const
   {
     return a + b;
   }
   template<>
   inline double operator()(double a, int b) const
   {
     return a + static_cast<double>(b);
   }
   template<>
   inline std::string operator()(std::string a, 
                                 char b) const
   {
     return std::operator+(a, b);
   }
   template<>
   inline std::string operator()(std::string a, 
                                 std::string b) const
   {
     return std::operator+(a, b);
   }
};

We see, in this snippet, the primary template for the function-call operator, and several full explicit specializations: for two int values, for a double and an int, for a string and a char, and for two string objects. But what if we want to restrict the use of the generic lambda l2 to arguments of the same type? This is not possible. The compiler cannot deduce our intention and, therefore, it would generate a different type template parameter for each occurrence of the auto specifier in the parameter list. However, the lambda templates from C++20 do allow us to specify the form of the function-call operator. Take a look at the following example:

auto l5 = []<typename T>(T a, T b) { return a + b; };
auto v1 = l5(42, 1);        // OK
auto v2 = l5(42, 1.0);      // error
auto v4 = l5(42.0, 1.0);    // OK
auto v5 = l5(42, false);    // error
auto v6 = l5(std::string{ "42" }, std::string{ "1" }); // OK 
auto v6 = l5(std::string{ "42" }, '1'); // error               

Invoking the lambda template with any two arguments of different types, even if they are implicitly convertible such as from int to double, is not possible. The compiler will generate an error. It's not possible to explicitly provide the template arguments when invoking the template lambda, such as in l5<double>(42, 1.0). This also generates a compiler error.

The decltype type specifier allows us to tell the compiler to deduce the type from an expression. This topic is covered in detail in Chapter 4, Advanced Template Concepts. However, in C++14, we can use this in a generic lambda to declare the second parameter in the previous generic lambda expression to have the same type as the first parameter. More precisely, this would look as follows:

auto l4 = [](auto a, decltype(a) b) {return a + b; };

However, this implies that the type of the second parameter, b, must be convertible to the type of the first parameter, a. This allows us to write the following calls:

auto v1 = l4(42.0, 1);                  // OK
auto v2 = l4(42, 1.0);                  // warning
auto v3 = l4(std::string{ "42" }, '1'); // error

The first call is compiled without any problems because int is implicitly convertible to double. The second call compiles with a warning, because converting from double to int may incur a loss of data. The third call, however, generates an error, because char cannot be implicitly convertible to std::string. Although the l4 lambda is an improvement over the generic lambda l2 seen previously, it still does not help restrict calls completely if the arguments are of different types. This is only possible with lambda templates as shown earlier.

Another example of a lambda template is shown in the next snippet. This lambda has a single argument, a std::array. However, the type of the elements of the array and the size of the array are specified as template parameters of the lambda template:

auto l = []<typename T, size_t N>(
            std::array<T, N> const& arr) 
{ 
   return std::accumulate(arr.begin(), arr.end(), 
                          static_cast<T>(0));
};
auto v1 = l(1);                           // error
auto v2 = l(std::array<int, 3>{1, 2, 3}); // OK

Attempting to call this lambda with anything other than an std::array object produces a compiler error. The compiler-generated function object may look as follows:

struct __lambda_5
{
   template<typename T, size_t N>
   inline auto operator()(
      const std::array<T, N> & arr) const
   {
     return std::accumulate(arr.begin(), arr.end(), 
                            static_cast<T>(0));
   }
   template<>
   inline int operator()(
      const std::array<int, 3> & arr) const
   {
     return std::accumulate(arr.begin(), arr.end(), 
                            static_cast<int>(0));
   }
};

An interesting benefit of generic lambdas over regular lambdas concerns recursive lambdas. Lambdas do not have names; they are anonymous, therefore, you cannot recursively call them directly. Instead, you have to define a std::function object, assign the lambda expression to it, and also capture it by reference in the capture list. The following is an example of a recursive lambda that computes the factorial of a number:

std::function<int(int)> factorial;
factorial = [&factorial](int const n) {
   if (n < 2) return 1;
      else return n * factorial(n - 1);
};
factorial(5);

This can be simplified with the use of generic lambdas. They don't require a std::function and its capture. A recursive generic lambda can be implemented as follows:

auto factorial = [](auto f, int const n) {
   if (n < 2) return 1;
   else return n * f(f, n - 1);
};
factorial(factorial, 5);

If understanding how this works is hard, the compiler-generated code should help you figure it out:

struct __lambda_6
{
   template<class T1>
   inline auto operator()(T1 f, const int n) const
   {
     if(n < 2) return 1;
     else return n * f(f, n - 1);
   }
   template<>
   inline int operator()(__lambda_6 f, const int n) const
   {
     if(n < 2) return 1;
     else return n * f.operator()(__lambda_6(f), n - 1);
   }
};
__lambda_6 factorial = __lambda_6{};
factorial(factorial, 5);

A generic lambda is a function object with a template function-call operator. The first argument, specified with auto, can be anything, including the lambda itself. Therefore, the compiler will provide a full explicit specialization of the call operator for the type of the generated class.

Lambda expressions help us avoid writing explicit code when we need to pass function objects as arguments to other functions. The compiler, instead, generates that code for us. Generic lambdas, introduced in C++14, help us avoid writing the same lambdas for different types. The lambda templates for C++20 allow us to specify the form of the generated call operator with the help of template syntax and semantics.

Summary

This chapter was a walk through of the core features of C++ templates. We have learned how to define class templates, function templates, variable templates, and alias templates. Along the way, we looked in detail at template instantiation and template specialization after learning about template parameters. We also learned about generic lambdas and lambda templates and what benefits they have compared to regular lambdas. By completing this chapter, you are now familiar with the template fundamentals, which should allow you to understand large parts of template code as well as write templates yourself.

In the next chapter, we will look at another important topic, which is templates with a variable number of arguments called variadic templates.

Questions

  1. What category of types can be used for non-type template parameters?
  2. Where are default template arguments not allowed?
  3. What is explicit instantiation declaration and how does it differ syntactically from explicit instantiation definition?
  4. What is an alias template?
  5. What are template lambdas?

Further reading

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset