55 / 163 · C++17 · 9 min
Parameter packs and fold expressions: handle empty packs first
Variadic templates preserve the type of each argument; fold expressions combine a parameter pack into a single expression. Reliable design must first decide the empty-pack result, the initial-value type, and evaluation order. Do not treat the ellipsis as an automatically safe loop or as an array of arbitrary length.
In this lesson
A parameter pack is not a runtime container
C++11's class... Ts declares a type parameter pack, and Ts... values forms the corresponding function parameter pack. A pack may have zero elements, and each element may have a different type; sizeof...(Ts) gives the count. Expansion generates a set of syntactic constructs in allowed positions; it does not produce an array that you can index at will.
Unlike C-style ellipsis, variadic templates retain type information, so each argument's constraints can be checked at instantiation. If the data is already a runtime sequence of the same type, a vector or span is often more appropriate. Do not turn a runtime problem into unbounded template instantiations just to avoid writing one loop. A zero-length pack is a normal case, not an afterthought: any interface that accepts a pack must still be valid when no arguments arrive. Expansion is not a runtime foreach over stored elements, and the arguments are not later available as a subscriptable array.
Fold direction and the initial value both affect the result
C++17's (init + ... + values) is a left fold with an initial value, equivalent to combining items one by one starting from init. Non-associative operations such as subtraction especially require distinguishing left folds from right folds. Even for addition, floating-point rounding and user-defined operators may make different associations produce different results.
Empty packs must be designed first. Unary folds without an initial value have a specified empty-pack result only for the built-in logical AND, logical OR, and comma operators. Summation should usually supply zero explicitly. The example uses 0LL and first converts arguments to long long, so an initial int zero does not accidentally fix a narrower type for the intermediate operations. Choosing that initial value also chooses the identity element of the intended operation and the type in which the fold begins. A well-typed empty result is part of the function's contract, not a detail the ellipsis will fill in automatically.
Order depends on the operator, not on appearance
Parentheses specify association structure; they do not magically add a left-to-right evaluation guarantee for every operator. If the expanded operations have side effects, choose an operator that already has the sequencing rules you need, for example a comma fold for void-returning operations. Logical folds over built-in bool can use short-circuit evaluation.
The example only handles small integers that are clearly representable, and it uses compile-time assertions to check the empty pack and multiple arguments. A wide type is not infinite precision: a long long sum can still overflow. Generic tools should restrict inputs or check for overflow. Do not read "the template supports many types" as "numerically safe for arbitrary values." Compact ... syntax is still an expression whose value, type, and side-effect order come from the operator and the initial value you wrote, including the empty-pack case decided in advance.
Pitfalls
(... + values)has no well-formed result for an empty pack; if the interface allows zero arguments, provide a suitable initial value explicitly.- The fold's association direction is not operand evaluation order; mutating shared state inside an addition fold makes the code hard to reason about correctly.
Run an example
Minimum C++17 · complete program · Download .cpp
#include <cassert>
#include <iostream>
#include <type_traits>
template<class... Ts>
constexpr long long sum(Ts... values) {
static_assert((std::is_integral_v<Ts> && ...), "integer inputs only");
return (0LL + ... + static_cast<long long>(values));
}
template<class... Ts>
constexpr bool all_positive(Ts... values) {
return ((values > 0) && ...);
}
int main() {
static_assert(sum() == 0);
static_assert(sum(1, 2, 3) == 6);
static_assert(all_positive());
assert(all_positive(1, 2, 3));
assert(!all_positive(1, 0, 3));
std::cout << sum() << ' ' << sum(1, 2, 3) << '\n';
}
Compile locally
g++ -std=c++17 -Wall -Wextra -Wpedantic -pthread modern-variadic.cpp -o example && ./exampleExpected result
0 6
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
If you change sum into a product function, what initial value should you choose when an empty parameter pack is allowed? Why should an empty pack not simply return zero?
Show a reference answer
Use (1LL * ... * static_cast<long long>(values)) so the empty pack returns the multiplicative identity 1. Then product(2, 3) is 6 and product() is 1, and merging two argument groups still obeys the product rule. An initial zero would swallow any non-empty product into zero. Type constraints and overflow bounds still need to be kept.
Check the sources
Drafts and official chapters change. The version mark is only the example’s minimum.