Variadic template patterns - part one
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
⁘ Basics
⁘ 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
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;
}
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
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;
}
Fold expressions
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;
}
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 operatorcout << " " << tail
: variadic expression, repeated as many times as there are elements intail
, separated by the,
operator
Template template parameters
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;
}