This series of short blog articles simply gathers in one place all the different usage patterns for variadic template expressions, so that it can be used as a reference (mainly by me) as needed.

Content

Introduction

Basics

Argument expansion

Fold expressions

Template template parameters

Introduction

C++11 introduces variable length template argument lists, meaning that the list of template arguments does not have to be fully specified using an actual type, the ... operator can be used instead i.e.

template <typename...ArgsT>

instead of

template <typename T1, typename T2>

and it does work equally well with non-type template parameters.

Basics

top

The ... operator causes the replacement, of Expression... with the actual comma separated list of template expressions defined at compile time.

The standard way of expanding the parameter pack at compile time is to employ pattern matching, with recursive calls and a termination condition:

#include <iostream>

using namespace std;

// termination condition
void FooImpl() {}

// pattern matching: ArgsT... --> Head, ArgsT... - extract and consume head
template <typename HeadType, typename...TailTypes>
void FooImpl(HeadType&& head, TailTypes&&...tail) {
    cout << "called with head = " << head << endl;
    FooImpl(std::forward<TailTypes>(tail)...);
}

// varargs function, relies on helper function to extract and process
// one element at a time
template <typename... ArgsT>
void Foo(ArgsT&&...args) {
    FooImpl(std::forward<ArgsT>(args)...);
}

int main() {
  
  Foo(1,2,3,4, "hello");
  
  return 0;
}
run

The line

FooImpl(std::forward<ArgsT>(args)...);

gets expanded to:

void FooImpl<int, int, int, int, char const (&) [6]>(int&&, int&&, int&&,
                                                     int&&, char const (&) [6])

Argument expansion

top

Now, trying to trigger an expansion by writing

...
template <typename T>
void Print(const T&, char sep = ' ') {
    cout << T << sep;
}
...
// pattern matching: ArgsT... --> Head, ArgsT... - extract and consume head
template <typename HeadType, typename...TailTypes>
void FooImpl(HeadType&& head, TailTypes&&...tail) {
    cout << "called with head = " << head << " tail = ";

    Print(tail)...; // <<<<<!

    FooImpl(std::forward<TailTypes>(tail)...);
}
...

will not work because in order for the variadic expression to be expanded it has to be consumed by some other callable entitiy or initializer list, which in turn requires the expression to be returning a value.

The following code will do the trick:

#include <iostream>

using namespace std;

template <typename T>
int Print(const T& v) { // added return value
    cout << ' ' << v;
    return 0;
}

// termination condition
void FooImpl() {}

// pattern matching: ArgsT... --> Head, ArgsT... - extract and consume head
template <typename HeadType, typename...TailTypes>
void FooImpl(HeadType&& head, TailTypes&&...tail) {
    cout << "called with head = " << head << " tail =";
    //---------------------------------------------------------------------
    struct Expand {
        Expand(...) {} // C va_args list
    };

    Expand{Print(tail)...}; // <<<<! Trigger expansion
    //---------------------------------------------------------------------

    cout << endl;
    FooImpl(std::forward<TailTypes>(tail)...);
}

// varargs function, relies on helper function to extract and process
// one element at a time through the standard `list` --> `head:tail`
// pattern matching
template <typename... ArgsT>
void Foo(ArgsT&&...args) {
    FooImpl(std::forward<ArgsT>(args)...);
}

int main() {
  
  Foo(1,2,3,4, "hello");
  
  return 0;
}
run

Fold expressions

top

Instead of triggering an expansion by passing the vararg expression as an argument to a callable entity, fold expressions, available starting with C++17 can be used:

#include <iostream>

using namespace std;

// termination condition
void FooImpl() {}

// pattern matching: ArgsT... --> Head, ArgsT... - extract and consume head
template <typename HeadType, typename...TailTypes>
void FooImpl(HeadType&& head, TailTypes&&...tail) {
    cout << "called with head = " << head << " tail =";
    //-------------------------------------------------------------------------
    (...,(cout <<  " " << tail)); // <<<<<!
    //-------------------------------------------------------------------------
    cout << endl;
    FooImpl(std::forward<TailTypes>(tail)...);
}

// varargs function, relies on helper function to extract and process
// one element at a time
template <typename... ArgsT>
void Foo(ArgsT&&...args) {
    FooImpl(std::forward<ArgsT>(args)...);
}

int main() {
  
  Foo(1,2,3,4, "hello");
  
  return 0;
}
run

Fold expressions are specified as:

... <operator> <vararg expression>`

where the ... operator causes the expansion of the variadic template expression following the operator.

In the code above (...,(cout << " " << tail)) is interpreted as:

  • ...: fold operator
  • ,: comma operator
  • cout << " " << tail: variadic expression, repeated as many times as there are elements in tail, separated by the , operator

Template template parameters

top

When specifying template template parameters in parameter lists, without using the ellipsis operator, you are required to know how many template parameters the template template parameter type accepts; using a variadic template list makes the code more generic:

#include <iostream>
#include <cstdlib>
#include <vector>

using namespace std;

template <class T, size_t Alignment = sizeof(T)>
class AlignedAllocator { // >= C++11: 
                         // allocator_traits<allocator<T>> fills defaults
public:
    static constexpr size_t alignment = Alignment;
    using value_type = T;
    template <class U> struct rebind {
        using other = AlignedAllocator<U, alignment>;
    };
    AlignedAllocator() = default;
    template <class U> 
    AlignedAllocator(AlignedAllocator<U, Alignment> const&) noexcept {}
    value_type*  allocate(std::size_t n) {
        void* aa = aligned_alloc(alignment, n*sizeof(value_type));
        return static_cast<value_type*>(aa);
    }
    void deallocate(value_type* p, std::size_t) noexcept {
        ::operator delete(p);
    }
};

template < template <typename...ArgsT> typename ContainerT,
           typename...ArgsT >
struct Stack {
    using ContainerType = ContainerT<ArgsT...>;
    using ValueType = typename ContainerType::value_type;
    ContainerType container;
    void Push(const ValueType& v) {container.push_back(v);}
    ValueType Pop() {
        ValueType v = container.back();
        container.pop_back();
        return v;
    }
};

int main() {
  Stack<vector, float, AlignedAllocator<float, 4>> s;
  s.Push(2.4f);
  cout << s.Pop() << endl;
  return 0;
}

run