Template programming

C++
Author

Quasar

Published

November 10, 2024

C++11 introduced variadic templates which permit functions to accept a variable number of arguments. They also permit template types such as std::tuple that can hold a variable number of elements. The main language mechanism enabling variadic templates is parameter packs, which hold an arbitrary number of values or types. Some things are easier to do with parameter packs - for instance passing the values they comprise to a function. Other tasks are a bit trickier to accomplish, such as iterating over a parameter pack or extracting specific elements. However, these things can generally be accomplished through various idioms, some more unwieldy then others.

Between C++11 and C++20, the language gained several improvements to variadic templates. Improvements to other features, such as concepts and lambdas, have also created new options for manipulating parameter packs in C++20. Ideally, cataloging these tricks make it easier for people to do what they need with variadic templates.

An overview of variadic templates

A template parameter pack is a template parameter that accepts zero or more template arguments. A function parameter pack is a function parameter that accepts zero or more function arguments. A variadic template is template that captures a parameter pack in its template arguments or function arguments. A parameter pack is captured by introducing an identifier prefixed by an ellipsis, as in ...X. Once captured, a parameter pack can later be used in a pattern expanded by an ellipsis (...), generally to the right of the pattern, as in X.... Pack expansion is conceptually equivalent to having one copy of the pattern for each element of the parameter pack.

#include <iostream>

template <typename T>
T sum(T x){
    return x;
}

template <typename T, typename... Args>
T sum(T x, Args... args){
    return x + sum<Args...>(args...);
}

int main()
{   
    double result = sum(1.0, 2.0, 3.0, 4.0, 5.0);
    std::cout << "result = " <<  result;
    return 0;
}

Compiler Explorer

The sum() function takes one or more arguments. The first argument is always captured by the parameter x and the rest of the arguments are captured by the pack ...args on line 9.

Expanding parameter packs

When using a variadic template, we often use a recursive logic with two overloads : one for the general case and one for ending the recursion. For instance:

#include <iostream>

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

template <typename T, typename... Args>
T min(T first, Args... rest){
    return min(first, min(rest...));
}

int main()
{
    int a = 2, b = 3, c = 4, d = 5;
    int minValue {0};
    minValue = min(a, b);
    minValue = min(a, b, c);
    minValue = min(a, b, c, d);
    return 0;
}

Compiler Explorer

The below code snip is a minimalistic example of tuple. The first class is the primary template. The primary template tuple has two member variables : first of type Type and rest of type Types... . This means that a template of N elements will contain the first element, and another tuple; this second tuple in turn contains the second element and yet another tuple; so on and so forth.

A captured parameter pack must be used in a pattern that is expanded with an ellipsis (...). A pattern is a set of tokens containing the identifiers of one or more parameter packs. On line 11, we capture a parameter pack rest consisting of a sequence of values rest[i] each of type Types[i] for the i-th position in parameter pack Types. On line 13, we expand the pattern rest.

// Variadic class templates and parameter pack expansion
#include <functional>
#include <utility>
#include <iostream>

template <typename Type, typename... Types>
struct tuple{
    Type first_;
    tuple<Types...> rest_;

    tuple(Type first, Types... rest) 
        : first_(first)
        , rest_(rest...)
        {}
};

template <typename T>
struct tuple<T>{
    T first_;

    tuple(T first) : first_(first) {}
};

int main()
{   
    tuple<double, double, double> x1(3.0, 4.0, 5.0);
    return 0;
}

Compiler Explorer

When a pattern contains more than one parameter pack, all packs must have the same length. This length determines the number of times the pattern is conceptually replicated in the expansion, once for each position in the expanded pack(s). Consider the following code snippet:

// An example with two parameter packs
#include <iostream>
#include <type_traits>
#include <tuple>

template <std::same_as<char>... C>
void expand(C... c)
{
    std::tuple<C...> tpl(c...);

    const char msg[] = { C(std::toupper(c))..., '\0' };
    //Do something
}
int main()
{   
    expand('t','e','m','p','l','a','t','e','s');
    return 0;
}

On line 7, tuple<C...> expands the pack C in the template-argument list, while tpl(c...) expands c in an initializer list (which, not to be confused with std::initializer_list is the C++ grammar for comma-separated lists of expressions passed as arguments to function calls and constructors).

On line 9, we expand the pattern C(std::toupper(c)) in another initializer list. This is an example of a pattern with two packs, C and c, both of which have the same length and are expanded in lockstep. (std::toupper() returns an int rather than a char so requires a cast).

sizeof...(pack)

The number of arguments in a parameter pack can be retrieved at compile-time with the sizeof... operator. This operator returns a constexpr value of the std::size_t type. Let’s see this in action:

#include <iostream>
#include <array>
template <typename... Args>
constexpr auto get_type_sizes(Args... args){
    return std::array<std::size_t, sizeof...(Args)>{sizeof(args)...};
}

int main()
{
    auto sizes = get_type_sizes<char, int, long, double>('a', 2, 3L, 3.14);
    return 0;
}

Compiler Explorer

In this snippet, sizeof...(Args) evaluates to \(4\) at compile-time, while sizeof(args)... is expanded to the following comma-separated pack of arguments: sizeof(char), sizeof(int), sizeof(long) and sizeof(double).

In most cases, an expanded pattern is conceptually equivalent to the number of copies of the pattern equal to the size of the parameter pack. Unless otherwise noted, a pattern is expanded by appending an ellipsis (...). Here is a list of contexts in which a pattern can be expanded:

  • Inside template parameters and function parameters, a pack expansion behaves like a comma separated list of patterns. An example in template parameters is the expansion of T in inner here:
template <typename... T>
struct outer{
    template <T... args>
    struct inner{};
};

outer<int, double, char[5]> a{};

An example in function parameters is the expansion of Args..., when you call foo:

template <typename... Args>
void foo(Args... args){}

foo(42);
foo(42, 'a');
  • In template argument lists as in std::tuple<C...>, the pack expands to the equivalent of a comma separated list of template arguments.

  • In function argument lists when a captured parameter pack appears inside the parenthesis of a function call. The largest expression to the left of the ellipsis (...) is the pattern that is expanded.

template<typename T>
T step_it(T value){
    return value + 1;
}
T sum(T x){
    return x;
}

T sum(T first, T... args){
    return (first + sum(args...));
}

template <typename... T>
void do_sums(T... args)
{
    auto s1 = sum(args...); 
    // sum(1, 2, 3, 4)

    auto s2 = sum(42, args...);
    // sum(42, 1, 2, 3, 4)

    auto s3 = sum(step_it(args)...);
    // sum(2, 3, 4, 5)
}

do_sums(1, 2, 3, 4);
  • In base specifier lists, to specify one base class for each member of a type parameter pack e.g.:
template <typename Base...>
struct MyStruct : Base...{
    MyStruct();
};
  • When initializing base classes in a mem-initializer list in a class constructor, the pack expansion initializes a list of base classes based on a type parameter pack:
template<typename... Base>
struct MyStruct: Base...{
    /* Default c'ctor */
    MyStruct() : Base...() {}

    MyStruct(const Base&... args) : Base{args}... {}
};
  • In initializer lists, the pack exmpansion is conceptually equivlent to a comma-separated list of instances of the pattern.
#include <iostream>
#include <array>

template<typename... Args>
struct sum_wrapper{
    sum_wrapper(Args... args){
        result = (... + args);
    }
    std::common_type_t<Args...> result;
};

template<typename... T>
void parenthesized(T... args){
    std::array<std::common_type_t<T...>,sizeof...(T)> arr {args...};
    //std::array<int, 4> {1, 2, 3, 4}

    sum_wrapper sw1(args...);
    //value = 1 + 2 + 3 + 4

    sum_wrapper sw2(++args...);
    //value = 2 + 3 + 4 + 5
}

int main()
{
    parenthesized(1, 2, 3, 4);
    return 0;
}

Compiler Explorer

  • In the context of deriving from a pack of base classes, it is useful to introduce names from the base classes into the definition of the derived class. Therefore, a pack expansion may also appear in a using declaration.
#include <iostream>
#include <array>

struct A{
    void execute() { std::cout << "A::execute()\n"; }
};

struct B{
    void execute() { std::cout << "B::execute()\n"; }
};

struct C{
    void execute() { std::cout << "C::execute()\n"; }
};

template<typename... Bases>
struct X : public Bases...
{
    X(Bases const& ... args) : Bases(args)... {}
    using Bases::execute...;
    // Conceptually equivalent to
    // using A::f;
    // using B::f;
    // using C::f;
};

int main()
{
    A a; B b; C c; X x(a, b, c);
    x.A::execute();
    x.B::execute();
    x.C::execute();

    
    return 0;
}

Compiler Explorer

  • Lambda Captures - The capture clause of a lambda expression may contain a pack expansion.
#include <iostream>

template<typename... Args>
std::common_type_t<Args...> add(Args... args){
    return (... + args);
}

template<typename... T>
void captures(T... args){
    auto l = [args...]{
        return add(args...);
    };

    l();
}

int main()
{
    captures(1, 2, 3, 4);
    return 0;
}
  • Fold expressions - These are similar to left fold and right fold in functional programming.
template<typename... T>
int sum(T... args){
    return (args + ...);
}

A pattern may itself contain an expanded parameter pack, in which case there is no need for the inner and outer packs to contain the same number of elements. The expanded inner pack simply becomes a part of the pattern around the outer pack. For example:

#include <iostream>

template<typename... Args>
std::common_type_t<Args...> sum(Args... il){
    return (... + il);
}

template<int... N>
struct Nested_sum{

    template<typename... Args>
    int nested_sum(Args... args){
        return sum(sum(N...,args)...);
    }
};
int main()
{
    Nested_sum<1,2> ns{};
    int result = ns.nested_sum(100, 200);
    // Equivalent to : sum(sum(1, 2, 100), sum(1, 2, 200))
    return 0;
}

Compiler Explorer

Implementing get<N> for the tuple

We can implement get<N> that takes a tuple as an argument and returns a reference to the element at the index n. Its prototype could look like the following:

template <int n, typename... Ts>
typename nth_type<n, Ts...>::value_type& get(tuple<Ts...>& t);

The template arguments are the index and a parameter pack of the tuple types. Its implementation, however, requires some helper types. First, we need to know what the type of the element is at the n-th index. This can be done with the help of the following nth_type variadic class template:

template<int n, typename T, typename... Ts>
struct nth_type : nth_type<n-1,Ts...>{
};

template<typename T, typename... Ts>
struct nth_type<0,T,Ts...>{
    using value_type = T;
};

Again, we have a primary template that uses recursive inheritance, and the specialization for the index 0 (which is the head of the list of templates). This type is only used as a mechanism for determining the type of a tuple element. We need another variadic class template for retrieving the value.

template<int n>
struct getter{
    template<typename... Ts>
    static typename nth_type<n, Ts...>::value_type&
    get(tuple<Ts...>& t){
        return getter<n-1>::get(t.rest_);
    }
};

template<>
struct getter<0>{
    template<typename T, typename... Ts>
    static T& get(tuple<T, Ts...>& t){
        return t.first_;
    }
};

With all these defined, we can now provide an implementation for the helper variadic function template get. This implementation relies on the getter class template and calls the get variadic function template.

template<int n, typename... Ts>
typename nth_type<n, Ts...>::value_type &
get(tuple<Ts...>& t){
    return getter<n>::get(t);
}

Compiler Explorer

Analysing the example above in cppinsights.io will be very illuminating. Here’s the listing:

/*************************************************************************************
 * NOTE: This an educational hand-rolled transformation. Things can be incorrect or  *
 * buggy.                                                                            *
 *************************************************************************************/
#include <iostream>

template<typename T, typename ... Ts>
struct tuple
{
  T first_;
  tuple<Ts...> rest_;
  inline tuple(T first, Ts... rest)
  : first_{first}
  , rest_{tuple<Ts...>(rest... )}
  {
  }
  
};

/* First instantiated from: insights.cpp:6 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct tuple<int, double>
{
  int first_;
  tuple<double> rest_;
  inline tuple(int first, double __rest1)
  : first_{first}
  , rest_{tuple<double>(__rest1)}
  {
  }
  
};

#endif
/* First instantiated from: insights.cpp:6 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct tuple<double>
{
  double first_;
  inline tuple(double first)
  : first_{first}
  {
  }
  
};

#endif
/* First instantiated from: insights.cpp:53 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct tuple<char, int, double>
{
  char first_;
  tuple<int, double> rest_;
  inline tuple(char first, int __rest1, double __rest2)
  : first_{first}
  , rest_{tuple<int, double>(__rest1, __rest2)}
  {
  }
  
};

#endif

template<typename T>
struct tuple<T>
{
  T first_;
  inline tuple(T first)
  : first_(first)
  {
  }
  
};

The tuple<char, int, double> contains an int and a tuple<int, double>, which contains a int and tuple<double>, which in turn contains a double value.

Next, we have the nth_type class template, for which, again, we have a primary template and several specializations, as follows:


template<int n, typename T, typename ... Ts>
struct nth_type : public nth_type<n - 1, Ts...>
{
};

/* First instantiated from: insights.cpp:19 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct nth_type<1, int, double> : public nth_type<0, double>
{
};

#endif
/* First instantiated from: insights.cpp:19 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct nth_type<0, double>
{
  using value_type = double;
};

#endif
/* First instantiated from: insights.cpp:45 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct nth_type<2, char, int, double> : public nth_type<1, int, double>
{
};

#endif

template<typename T, typename ... Ts>
struct nth_type<0, T, Ts...>
{
  using value_type = T;
};

The nth_type<2, char, int, double> specialization is derived from nth_type<1, int, double> which in turn is derived from nth_type<0, double>, which is the last class in the base hierarchy.

The nth_type structure is used as the return type in the getter helper class template, which is instantiated as follows:

/* First instantiated from: insights.cpp:32 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct getter<1>
{
  template<typename ... Ts>
  static inline typename nth_type<1, Ts...>::value_type & get(tuple<Ts...> & t);
  
  /* First instantiated from: insights.cpp:32 */
  #ifdef INSIGHTS_USE_TEMPLATE
  template<>
  static inline typename nth_type<1, int, double>::value_type & get<int, double>(tuple<int, double> & t)
  {
    return getter<0>::get(t.rest_);
  }
  #endif
  
};

#endif
/* First instantiated from: insights.cpp:47 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct getter<2>
{
  template<typename ... Ts>
  static inline typename nth_type<2, Ts...>::value_type & get(tuple<Ts...> & t);
  
  /* First instantiated from: insights.cpp:47 */
  #ifdef INSIGHTS_USE_TEMPLATE
  template<>
  static inline typename nth_type<2, char, int, double>::value_type & get<char, int, double>(tuple<char, int, double> & t)
  {
    return getter<1>::get(t.rest_);
  }
  #endif
  
};

#endif

template<>
struct getter<0>
{
  template<typename T, typename ... Ts>
  static inline T & get(tuple<T, Ts...> & t)
  {
    return t.first_;
  }
  
  /* First instantiated from: insights.cpp:32 */
  #ifdef INSIGHTS_USE_TEMPLATE
  template<>
  static inline double & get<double>(tuple<double> & t)
  {
    return t.first_;
  }
  #endif
  
};

We see the use of the keyword typename to prefix the nth_type<N, Ts...>::value_type which is a dependent type. In C++ 20, this is however no longer necessary.

Because implementing variadic templates is often verbose and can be cumbersome, the C++17 added fold expressions.

Fold Expressions

A special form of pack expansions is folds introduced in C++17. Above, we showed a function sum that summed a set of integers. This function can be implemented far more concisely with a fold:

int sum(auto... i){
    return (... + i);
}

Let \(p_1,\ldots,p_n\) be the instances of the parameter pack \(p\). Let \(\bigoplus\) stand for any binary operator in the C++ grammar.

A binary left fold has the form \((e \bigoplus \ldots \bigoplus p)\) and is equivalent to \((((e \bigoplus p_1) \bigoplus p_2)\ldots ) \bigoplus p_n\).

A unary left fold has the form \((\ldots \bigoplus p)\) and is equivalent to \((((p_1 \bigoplus p_2)\bigoplus p_3) \ldots )\bigoplus p_n\).

A binary right fold has the form \((p \bigoplus \ldots \bigoplus e)\) and is equivalent to \(p_1 \bigoplus (\ldots (p_{n-1} \bigoplus (p_n \bigoplus e)))\).

A binary right fold has the form \((p \bigoplus \ldots \bigoplus p_n)\) and is equivalent to \(p_1 \bigoplus (p_2 \bigoplus (\ldots \bigoplus p_n))\).

In the above expressions, \(e\) stands for the initial value.

Idioms

Below is a collection of idioms for working with parameter packs.