Templates in C++

Dated May 30, 2022; last modified on Mon, 30 May 2022

A template is a class or a function that we can parameterize with a set of types or values.

Parameterized Types

The vector-of-doubles can be generalized to a vector-of-anything type by making it a template:

// `template<typename T>` can be read as "for all types T". Older code
// uses `template<class T>`, which is equivalent.
template<typename T>
class Vector {
 public:
  explicit Vector(int s);
  ~Vector() { delete[] elem; }

  // ... copy and move operations ...

  T& operator[](int i);               // For non-const Vectors
  const T& operator[](int i) const;   // For const Vectors
  int size() const { return sz; }

 private:
  T* elem;
  int sz;
};

Member functions are defined similarly, e.g.

template<typename T>
Vector<T>::Vector(int s) {
  if (s < 0)
    throw Negative_size{};

  elem = new T[s];
  sz = s;
}

A template plus a set of of template arguments is called an instantiation or a specialization, e.g. Vector<char>. Late in the compilation process, at instantiation time, code is generated for each instantiation used in a program. Using templates incurs no run-time overhead compared to hand-crafted code.

That templates are instantiated late in the compilation process may lead to unintuitive compiler error messages.

Constrained Template Arguments (C++20)

Constrained template arguments are useful when a template would only make sense for template arguments that meet a certain criteria. This is achieved using template<Element T>, which can be read as, “For all T such that Element(T)”.

Element is a predicate that checks whether T has all the properties that the template class requires. Such a predicate is called a concept. A template argument for which a concept is specified is called a constrained argument, and a template for which an argument is constrained is called a constrained template. Older code uses unconstrained template arguments and leaves requirements to documentation.

For example, suppose functions f, g, and h require that their arguments be hashable. We can do:

#include <string>
#include <cstddef>
#include <concepts>

template<typename T>
concept Hashable = requires(T a) {
  { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};

// One way of applying the Hashable constraint.
template<Hashable T>
void f(T) {}

struct Foo;

int main {
  f(std::string("s"));  // OK, std::string satisfies Hashable
  // f(Foo{});          // Error: Foo does not satisfy Hashable
}

This is similar to Haskell’s type classes. For example, the type of (==) is (==) :: Eq a => a -> a -> Bool, which can be read as, “For any type a, as long as a is an instance of Eq, (==) can take two values of type a and return a Bool.

However, there is a loose coupling that doesn’t exist in Haskell’s version. In Haskell, a matching type a may be something of the form:

data Foo = F Int | G Char
  deriving (Eq)

On the other hand, the C++ type that can be passed to f does not need to reference the Hashable concept anywhere in its code.

Value Template Arguments

In addition to to type arguments, a template can take value arguments (which must be constant expressions), e.g.

template<typename T, int N>
struct Buffer {
  // Convenience functions for accessng the template arguments.
  using value_type = T;
  constexpr int size() { return N; }

  T[N];
  // ...
};

Value arguments are useful in many contexts. For example, Buffer allows us to create arbitrarily sized buffers with no use of the free store.

Template Argument Deduction

Argument deduction can help reduce redundant typing, e.g.

Vector v1 {1, 2, 3};  // Deduce v1's element type from the initializer list element type
Vector v2 = v1;       // Deduce v2's element type form v1's element type
Vector<int> v3(1);    // Need to be explicit as no element type is mentioned

But deduction can also cause surprises, e.g.

Vector<string> vs1 {"Hello", "World"};  // Vector<string>
Vector vs {"Hello", "World"};           // Deduces to Vector<const char*>

When a template argument can’t be deduced from the constructor arguments, we can provide a deduction guide, e.g.

// Template declaration
template<typename T>
class Vector2 {
 public:
  using value_type = T;

  Vector2(std::initializer_list<T>);  // Initializer-list constructor

  template<typename Iter>
    Vector2(Iter b, Iter e);          // [b:e) range constructor
};

// Additional deduction guide
template<typename Iter>
  Vector2(Iter, Iter) -> Vector2<typename Iter::value_type>;

The user-defined deduction guide needs not be a template, e.g.

template<class T> struct S {
  S(T);
};
S(char const*) -> S<std::string>;

S s{"Hello"}; // Deduced to S<std::string>

The effects of deduction guides are often subtle, so limit their use; prefer using concepts.

References

  1. A Tour of C++ (Second Edition). Chapter 6. Templates. Bjarne Stroustrup. 2018. ISBN: 978-0-13-499783-4 .
  2. Constraints and concepts (since C++20) - cppreference.com. en.cppreference.com . Accessed May 30, 2022.
  3. 05-type-classes. Brent Yorgey. www.cis.upenn.edu . 2013. Accessed May 30, 2022.
  4. Class template argument deduction (CTAD) (since C++17) - cppreference.com. en.cppreference.com . Accessed May 30, 2022.