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 x){
T sumreturn x;
}
template <typename T, typename... Args>
(T x, Args... args){
T sumreturn 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;
}
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 a, T b)
T min{
return a < b ? a : b;
}
template <typename T, typename... Args>
(T first, Args... rest){
T minreturn min(first, min(rest...));
}
int main()
{
int a = 2, b = 3, c = 4, d = 5;
int minValue {0};
= min(a, b);
minValue = min(a, b, c);
minValue = min(a, b, c, d);
minValue return 0;
}
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{
first_;
Type <Types...> rest_;
tuple
(Type first, Types... rest)
tuple: first_(first)
, rest_(rest...)
{}
};
template <typename T>
struct tuple<T>{
first_;
T
(T first) : first_(first) {}
tuple};
int main()
{
<double, double, double> x1(3.0, 4.0, 5.0);
tuplereturn 0;
}
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()
{
('t','e','m','p','l','a','t','e','s');
expandreturn 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;
}
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
ininner
here:
template <typename... T>
struct outer{
template <T... args>
struct inner{};
};
<int, double, char[5]> a{}; outer
An example in function parameters is the expansion of Args...
, when you call foo
:
template <typename... Args>
void foo(Args... args){}
(42);
foo(42, 'a'); foo
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 value){
T step_itreturn value + 1;
}
(T x){
T sumreturn x;
}
(T first, T... args){
T sumreturn (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)
}
(1, 2, 3, 4); do_sums
- 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 */
() : Base...() {}
MyStruct
(const Base&... args) : Base{args}... {}
MyStruct};
- 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{
(Args... args){
sum_wrapper= (... + args);
result }
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}
(args...);
sum_wrapper sw1//value = 1 + 2 + 3 + 4
(++args...);
sum_wrapper sw2//value = 2 + 3 + 4 + 5
}
int main()
{
(1, 2, 3, 4);
parenthesizedreturn 0;
}
- 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...
{
(Bases const& ... args) : Bases(args)... {}
Xusing Bases::execute...;
// Conceptually equivalent to
// using A::f;
// using B::f;
// using C::f;
};
int main()
{
; B b; C c; X x(a, b, c);
A a.A::execute();
x.B::execute();
x.C::execute();
x
return 0;
}
- 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()
{
(1, 2, 3, 4);
capturesreturn 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()
{
<1,2> ns{};
Nested_sumint result = ns.nested_sum(100, 200);
// Equivalent to : sum(sum(1, 2, 100), sum(1, 2, 200))
return 0;
}
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&
(tuple<Ts...>& t){
getreturn 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 &
(tuple<Ts...>& t){
getreturn getter<n>::get(t);
}
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
{
first_;
T <Ts...> rest_;
tupleinline 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_;
<double> rest_;
tupleinline 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_;
<int, double> rest_;
tupleinline tuple(char first, int __rest1, double __rest2)
: first_{first}
, rest_{tuple<int, double>(__rest1, __rest2)}
{
}
};
#endif
template<typename T>
struct tuple<T>
{
first_;
T 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.