C++ Fold Expressions: Simplifying Variadic Templates
Fold expressions, introduced in C++17, provide a concise and powerful mechanism for unpacking variadic template parameter packs using binary operators. Before their introduction, processing variadic arguments required complex template recursion and base-case overloads, which cluttered codebases and increased compilation overhead. This article explains how fold expressions work, explores their four core syntax variants, and demonstrates how they streamline common programming patterns such as aggregation, boolean logic, and sequential function execution.
The Problem: Variadic Templates Before C++17
Variadic templates, introduced in C++11, allowed functions and classes to accept an arbitrary number of template arguments. However, operating on these arguments typically required recursive template instantiations combined with a terminating base-case function.
For example, computing the sum of arbitrary numbers previously required two separate functions:
// Base case
template<typename T>
T sum(T val) {
return val;
}
// Recursive case
template<typename T, typename... Args>
T sum(T first, Args... rest) {
return first + sum(rest...);
}This recursive idiom produced significant boilerplate, increased compilation times, and risked hitting compiler recursion limits on large packs.
What are Fold Expressions?
Fold expressions eliminate the need for recursive boilerplate by allowing developers to apply a binary operator across all elements of a parameter pack directly within a single expression. Under the hood, the compiler expands the pack and groups the operands according to the chosen fold variant.
C++ supports 32 operators in fold expressions, including arithmetic
(+, -, *, /),
logical (&&, ||), bitwise
(&, |, ^), relational
(<, ==), and the comma operator
(,).
The Four Fold Varieties
Fold expressions are categorized by two factors: whether they process elements from left-to-right or right-to-left, and whether they include an explicit initial value.
Unary Right Fold:
(pack op ...)Expands to:(arg1 op (... op (argN-1 op argN)))Unary Left Fold:
(... op pack)Expands to:(((arg1 op arg2) op ...) op argN)Binary Right Fold:
(pack op ... op init)Expands to:(arg1 op (... op (argN op init)))Binary Left Fold:
(init op ... op pack)Expands to:(((init op arg1) op arg2) op ... op argN)
Practical Applications
1. Mathematical Reductions
Using a unary left fold, summing numbers reduces to a single readable line without any base-case overloads:
template<typename... Args>
auto sum(Args... args) {
return (... + args); // Unary left fold
}If the parameter pack might be empty, a binary fold provides an initial value to prevent compilation errors:
template<typename... Args>
auto sum_with_default(Args... args) {
return (0 + ... + args); // Binary left fold
}2. Compile-Time Logical Assertions
Fold expressions excel at validating multiple conditions across a pack. For instance, verifying that all types passed to a function satisfy a concept or type trait:
template<typename... Args>
constexpr bool all_integral() {
return (std::is_integral_v<Args> && ...); // Unary right fold
}This performs short-circuit evaluation automatically; if any type
evaluates to false, evaluation halts immediately.
3. Executing Operations with the Comma Operator
The comma operator allows fold expressions to perform side-effects sequentially for each element in a pack, such as printing to an output stream:
template<typename... Args>
void print_all(const Args&... args) {
((std::cout << args << ' '), ...); // Unary right fold over the comma operator
std::cout << '\n';
}Summary of Benefits
Fold expressions transform variadic template programming in C++. They remove redundant base cases, eliminate deep template instantiation stacks, shorten build times, and yield clear, expressive code that conveys programmer intent directly.