Book Image

Template Metaprogramming with C++

By : Marius Bancila
5 (1)
Book Image

Template Metaprogramming with C++

5 (1)
By: Marius Bancila

Overview of this book

Learn how the metaprogramming technique enables you to create data structures and functions that allow computation to happen at compile time. With this book, you'll realize how templates help you avoid writing duplicate code and are key to creating generic libraries, such as the standard library or Boost, that can be used in a multitude of programs. The introductory chapters of this book will give you insights into the fundamentals of templates and metaprogramming. You'll then move on to practice writing complex templates and exploring advanced concepts such as template recursion, template argument deduction, forwarding references, type traits, and conditional compilation. Along the way, you'll learn how to write variadic templates and how to provide requirements to the template arguments with C++20 constraints and concepts. Finally, you'll apply your knowledge of C++ metaprogramming templates to implement various metaprogramming patterns and techniques. By the end of this book, you'll have learned how to write effective templates and implement metaprogramming in your everyday programming journey.
Table of Contents (16 chapters)
1
Part 1: Core Template Concepts
5
Part 2: Advanced Template Features
9
Part 3: Applied Templates
Appendix: Closing Notes

Writing your first templates

It is now time to see how templates are written in the C++ language. In this section, we will start with three simple examples, one for each of the snippets presented earlier.

A template version of the max function discussed previously would look as follows:

template <typename T>
T max(T const a, T const b)
{
   return a > b ? a : b;
}

You will notice here that the type name (such as int or double) has been replaced with T (which stands for type). T is called a type template parameter and is introduced with the syntax template<typename T> or typename<class T>. Keep in mind that T is a parameter, therefore it can have any name. We will learn more about template parameters in the next chapter.

At this point, this template that you put in the source code is only a blueprint. The compiler will generate code from it based on its use. More precisely, it will instantiate a function overload for each type the template is used with. Here is an example:

struct foo{};
int main()
{   
   foo f1, f2;
   max(1, 2);     // OK, compares ints
   max(1.0, 2.0); // OK, compares doubles
   max(f1, f2);   // Error, operator> not overloaded for 
                  // foo
}

In this snippet, we are first calling max with two integers, which is OK because operator> is available for the type int. This will generate an overload int max(int const a, int const b). Second, we are calling max with two doubles, which again is all right since operator> works for doubles. Therefore, the compiler will generate another overload, double max(double const a, double const b). However, the third call to max will generate a compiler error, because the foo type does not have the operator> overloaded.

Without getting into too many details at this point, it should be mentioned that the complete syntax for calling the max function is the following:

max<int>(1, 2);
max<double>(1.0, 2.0);
max<foo>(f1, f2);

The compiler is able to deduce the type of the template parameter, making it redundant to write it. There are cases, however, when that is not possible; in those situations, you need to specify the type explicitly, using this syntax.

The second example involving functions from the previous section, Understanding the need for templates, was the quicksort() implementation that dealt with void* arguments. The implementation can be easily transformed into a template version with very few changes. This is shown in the following snippet:

template <typename T>
void swap(T* a, T* b)
{
   T t = *a;
   *a = *b;
   *b = t;
}
template <typename T>
int partition(T arr[], int const low, int const high)
{
   T pivot = arr[high];
   int i = (low - 1);
   for (int j = low; j <= high - 1; j++)
   {
      if (arr[j] < pivot)
      {
         i++;
         swap(&arr[i], &arr[j]);
      }
   }
   swap(&arr[i + 1], &arr[high]);
   return i + 1;
}
template <typename T>
void quicksort(T arr[], int const low, int const high)
{
   if (low < high)
   {
      int const pi = partition(arr, low, high);
      quicksort(arr, low, pi - 1);
      quicksort(arr, pi + 1, high);
   }
}

The use of the quicksort function template is very similar to what we have seen earlier, except there is no need to pass pointers to callback functions:

int main()
{
   int arr[] = { 13, 1, 8, 3, 5, 2, 1 };
   int n = sizeof(arr) / sizeof(arr[0]);
   quicksort(arr, 0, n - 1);
}

The third example we looked at in the previous section was the vector class. A template version of it will look as follows:

template <typename T>
struct vector
{
   vector();
   size_t size() const;
   size_t capacity() const;
   bool empty() const;
   void clear();
   void resize(size_t const size);
   void push_back(T value);
   void pop_back();
   T at(size_t const index) const;
   T operator[](size_t const index) const;
private:
   T* data_;
   size_t size_;
   size_t capacity_;
};

As in the case of the max function, the changes are minimal. There is the template declaration on the line above the class and the type int of the elements has been replaced with the type template parameter T. This implementation can be used as follows:

int main()
{   
   vector<int> v;
   v.push_back(1);
   v.push_back(2);
}

One thing to notice here is that we have to specify the type of the elements when declaring the variable v, which is int in our snippet because the compiler would not be able to infer their type otherwise. There are cases when this is possible, in C++17, and this topic, called class template argument deduction, will be discussed in Chapter 4, Advanced Template Concepts.

The fourth and last example concerned the declaration of several variables when only the type was different. We could replace all those variables with a template, as shown in the following snippet:

template<typename T>
constexpr T NewLine = T('\n');

This template can be used as follows:

int main()
{
   std::wstring test = L"demo";
   test += NewLine<wchar_t>;
   std::wcout << test;
}

The examples in this section show that the syntax for declaring and using templates is the same whether they represent functions, classes, or variables. This leads us to the next section where we will discuss the types of templates and template terminology.