A detailed overview of Templates

I wrote the previous post in which I provided you all, a little bit of introduction into C++ templates. I also mentioned that, if the topic is well received, I will probably write more articles on this topic.

Well, here it is. In this post I’m going to give you a detailed overview on C++ templates, with articles on each type of template to follow later on.

Let me make this clear from now, I’m going to make use of the cppreference since it is the holy bible of C++ developers and will only take into consideration everything that is kept in the language after C++11.

Templates

A template is a C++ entity that defines one of the following:

  • a family of classes (class template), which may be nested classes
  • a family of functions (function template), which may be member functions
  • an alias to a family of types (alias template)(since C++11)
  • a family of variables (variable template)(since C++14)
  • a concept (constraints and concepts)(since C++20)

All of these entities(except for concepts) are have very similar implementation to their non-templatized counterparts but instead of using strictly values as parameters, you are going to use mostly data types as parameters. You can also use non types or other templates as parameters.

The big difference is that, the parameters are evaluated at compile time and of course, the syntax is different.

Syntax

template <parameter-list> requires-clause(optional) declaration (1)
template <parameter-list> concept concept-name = constraint-expression ; (2) (since C++20)

Description:

  • parameter-list – a non-empty comma-separated list of the template parameters, each of which is either non-type parameter, a type parameter, a template parameter, or a parameter pack of any of those.
  • requires-clause – (since C++20) a requires-clause that specifies the constraints on the template arguments.
  • declaration – declaration of a class (including struct and union), a member class or member enumeration type, a function or member function, a static data member at namespace scope, a variable or static data member at class scope, (since C++14) or an alias template (since C++11). It may also define a template specialization.
  • concept-name, constraint-expression – see constraints and concepts. We will go into more detail on these in the following articles on concepts.

Some examples

// class template with type parameter
template <class T>
class Container
{
public:
    Container(T val): val_(val) {}
private:
    T val_;
};
// function template with non-type parameter
// also has default value
template <int T = 1>
void foo()
{
    std::cout << T;
}
// function template with type paramater pack
// this also uses fold expression(C++17 onwards)
template <class... Args>
bool logical_and(Args... args) { return (... && args); }
// concept (C++20 onwards)
template <class T>
concept IntSized = (sizeof(int) == sizeof(T));

// and with this concept you can constrain parameter of other template
template <IntSized T>
T sum(T a, T b)
{
    return a + b;
}

// or like this
template <class T>
  requires IntSized<T>
T sum(T a, T b)
{
    return a + b;
}
template<class T>
struct Alloc { };

// alias template
template<class T>
using Vec = std::vector<T, Alloc<T>>;

You can find all the examples in one place at cppinsights.

These are just some examples so you can get a little bit accustomed with templates in general. Will go in more detail in future articles about each type of template.

Template instantiation

The act of creating a new definition of a function, class, or member of a class from a template declaration and one or more template arguments is called template instantiation.

Template instantiation has two forms: explicit instantiation and implicit instantiation.

Implicit instantiation

Unless a template specialization has been explicitly instantiated or explicitly specialized, the compiler will generate a specialization for the template only when it needs the definition. This is called implicit instantiation.

// class template with type parameter
template <class T>
class Container
{
    T val_;
};

No instantiation is done, as long as the class is not used (cppinsights). But, if we were to use the class, the compiler will instantiate the class for us – function definition will be generated.

  Container<int> x;
  Container<float> y;
  Container<double> z;

The compiler will instantiate for each of the above usages, a class definition (cppinsights).

However, if you were to have a pointer to the class, the compiler does not need to know the definition of the class. Therefore, no implicit instantiation is done.

  Container<int>* x;
  Container<float>* y;
  Container<double>* z;

None of the above will instantiate the template(cppinsights).

Explicit instantiation

The other type of instantiation is explicit instantiation, which means that, we will tell the compiler which signatures to generate for us. The explicit instantiation can be done with the template keyword and signature after. The exact syntax can be found in each template type page on cppreference.

template <class T>
class Container
{
public:
  void foo() {}; 

  T val_;
};

template <class T>
T sum(T a, T b)
{
  return a+b;
}

And we can explicitly instantiate these two templates.

template class Container<int>;
template void Container<double>::foo();
template int sum(int a, int b);
template double sum<double>(double a, double b);

Full example code on cppinsights.

Full template specialization

When you create a template, that template will behave the same for all template parameters provided(if that is possible and the compiler can substitute them).

For example:

template <class T>
T sum(T a, T b)
{
    return a + b;
}

If we use this template function, it will behave exactly the same for all accepted data types provided. The compiler will generate a function signature for us with each type that we use but with the same body. This can be checked on cppinsights.

However, let’s say we want to do something different for T = double. In this case, we can specialize the template for the type double. We will add one more definition for the specialization:

template <>
double sum(double a, double b)
{
    return a + b + 1.5;
}

In this case, the compiler did not generate the function signature for T = double but it found it directly, because it was implemented by us: cppinsights.

Partial template specialization

The same way we specialized the sum function above, we can also partially specialize a template. The catch here is that, we are only allowed to partially specialize a class, but not a function. The standard allows only full template specialization for functions.

// template definition
template <class T, class U>
struct X
{
  int sum()
  {
    return 1;
  }
};

// partial specialization
template <class T>
struct X<T, int>
{
  int sum()
  {
    return 2;
  }
};

Link to cppinsights.

More details by example

I will add here another example that should provide more insight on how templates work. Credit for the idea and example goes to user bziur on Reddit.

// declaration "blueprint"
// in header
// no code produced
// lets compiler know how to produce definitions
template <typename Type>
Type get(const QJsonDocument &doc, const QString &path);

// declaration specialization
// in header
// produces declaration
// lets compiler know "there's definition somewhere"
template <>
int get<int>(const QJsonDocument &doc, const QString &path);

// definition "blueprint"
// in header after declaration instantiations
// no code produced unless instantiated
// provides blueprint for undeclared function definitions
template <typename Type>
Type get(const QJsonDocument &doc, const QString &path, const Type &defaultValue) {
  try { 
    return get<Type>(doc, path);
  } catch (const std::invalid_argument &) {
    return defaultValue;
  }
}

// definition specialization
// in source
// produces definition
// results in instantiation, produces function that You can put in a library
template <>
int get<int>(const QJsonDocument &doc, const QString &path) {
  auto json = get<QJsonValue>(doc, path);
  if (!json.isDouble()) {
    throw std::invalid_argument(getConvertFailedMessage<int>(path, json.toString()));
  }
  return json.toInt();
} 

In conclusion

This is it for the detailed overview of templates in C++. I did not go into very detailed information on any of the template types since I plan on writing articles on each one.

The article is more about an overview of templates as a concept, what you can do with them and how you can and should use them.

I hope you learned something new and/or you enjoyed the read.

2 thoughts on “A detailed overview of Templates”

  1. Pingback: Concepts and SFINAE: Compilation time - cppdev

  2. Pingback: Constrain users with concepts (C++20) - cppdev

Leave a Reply

%d bloggers like this: