20 minutes
Fold Expressions in C++17: Simplifying Variadic Template Code
Introduction
Variadic templates, introduced in C++11, enabled functions and classes to accept an arbitrary number of arguments, but using them often required cumbersome recursion or expansion tricks. C++17 addressed this complexity by introducing fold expressions, which provide a concise way to reduce (or “fold”) a parameter pack over a binary operator (Fold expressions (since C++17) - cppreference.com). In this article, I explore what fold expressions are, how they simplify variadic template code, and how to use them effectively. I will explain their syntax (unary left/right folds vs. binary folds), demonstrate several examples (summing values, computing logical conjunctions, finding a minimum value), and discuss common use cases, best practices, and pitfalls. The tone is analytical and professional, assuming an intermediate to advanced understanding of C++. By the end, you should have a thorough grasp of fold expressions and be ready to apply them in your modern C++ code.
What Are Fold Expressions?
Fold expressions are a language feature (added in C++17) that automatically expands a parameter pack with a given binary operator, producing a single combined result. In essence, a fold expression applies a binary operator repeatedly to each template argument in a pack, effectively reducing a pack of values down to one value (Fold expressions (since C++17) - cppreference.com). The name “fold” comes from functional programming, where fold (or reduce) operations combine a sequence of elements using a function or operator to produce a single result. C++17 brings this capability into the core language, allowing the compiler to generate the necessary code to combine all arguments, rather than the programmer writing explicit loops or recursive templates.
Before fold expressions, programmers had to expand parameter packs manually. For example, to sum an arbitrary list of numbers, one common approach was to write a recursive variadic template or use an std::initializer_list
trick. Such solutions were verbose and error-prone. Fold expressions simplify this by letting the compiler generate the boilerplate. As a result, code that accumulates or processes pack arguments becomes shorter, clearer, and less likely to harbor bugs. In this article, I will use first-person pronouns to guide you through understanding and using fold expressions, drawing on my experience to explain their mechanics and nuances in a formal yet approachable manner.
Syntax and Variants of Fold Expressions
The C++17 standard defines four forms of fold expressions, distinguished by whether they have an initial value and whether the fold is left-associative or right-associative. The general syntax uses an ellipsis (...
) on either the left or right of the parameter pack, optionally with an initial seed value. The four variants are (Fold expressions (since C++17) - cppreference.com):
- Unary left fold:
(... op pack)
– the operatorop
is applied left-associatively across the pack. - Unary right fold:
(pack op ...)
– the operator is applied right-associatively across the pack. - Binary left fold:
(init op ... op pack)
– like a left fold but with an initial valueinit
that is placed to the left of the pack. - Binary right fold:
(pack op ... op init)
– like a right fold but with an initial value on the right of the pack.
In these forms, pack is an expression containing the parameter pack (the expanded variadic arguments), op is the binary operator used for folding, and init is an initial value (an expression that does not contain the pack) (Fold expressions (since C++17) - cppreference.com). Importantly, the opening and closing parentheses around the entire fold expression are required by the syntax (Fold expressions (since C++17) - cppreference.com).
The fold operator op
can be any of the 32 allowed binary operators in C++ (for example, +
, -
, *
, /
, %
, ^
, &
, |
, &&
, ||
, ==
, <
, >
, <<
, >>
, the comma ,
, etc.) (Fold expressions (since C++17) - cppreference.com). In a binary fold (one with an initial value), the same operator must appear on both sides of the ellipsis (Fold expressions (since C++17) - cppreference.com). This means you cannot mix different operators in a single fold expression. For instance, (args + ... - init)
is not a valid fold, because the operator to the left of the pack (+
) is different from the one to the right (-
). Both must be the same in forms 3 and 4.
Associativity (Left vs. Right): The position of the ellipsis determines how the expression is parenthesised by the compiler. A unary left fold (... op pack)
expands to ((pack1 op pack2) op pack3) op ... op packN
(fully left-associative), whereas a unary right fold (pack op ...)
expands to pack1 op (pack2 op (pack3 op ... op packN))
(fully right-associative) (Fold expressions (since C++17) - cppreference.com). In other words, the left fold combines elements from left to right, and the right fold combines from right to left. For many associative operators (like addition or multiplication), left vs. right fold yields the same result. However, for non-associative or order-dependent operators, the difference is significant.
For example, consider a simple subtraction of a pack of numbers:
#include <iostream>
template<typename... Args>
auto leftFoldSubtract(Args... args) {
return (... - args); // unary left fold
}
template<typename... Args>
auto rightFoldSubtract(Args... args) {
return (args - ...); // unary right fold
}
int main() {
std::cout << leftFoldSubtract(10, 3, 2) << "\n"; // (10 - 3) - 2
std::cout << rightFoldSubtract(10, 3, 2) << "\n"; // 10 - (3 - 2)
}
In the leftFoldSubtract
call, the expansion is ((10 - 3) - 2)
, which yields 5
. In rightFoldSubtract
, the expansion is 10 - (3 - 2)
, which yields 9
. The results differ (5
vs 9
) because subtraction is not associative, so the order of folding matters (C++ Fold Expressions 101 - Fluent C++). This example illustrates that you must choose left or right folds carefully based on the operation’s properties. Generally, for left-associative semantics use the (... op pack)
form, and for right-associative semantics use (pack op ...)
. (If this reminds you of functional programming, C++’s left fold corresponds to Haskell’s foldl
, and the right fold corresponds to foldr
(From Variadic Templates to Fold Expressions – MC++ BLOG).)
Initial Values (Binary Folds): The binary fold variants allow specifying an initial value to be folded in. For instance, (init op ... op pack)
will start the fold by combining init
with the first element of the pack (for a left fold) or combining the last element of the pack with init
(for a right fold). Initial values serve as a base case or identity element for the fold. They are especially useful when the parameter pack might be empty, or when you want to ensure a certain result if no arguments are provided. For example, one could define a summation with an initial value 0:
template<typename... Args>
auto sumAll(Args... args) {
return (0 + ... + args); // binary left fold with init = 0
}
Here 0
is the initial value, so if sumAll
is called with no arguments, the result would just be 0
. In fact, certain operators have well-defined identity values that make sense as defaults. C++17 fold expressions define that for an empty pack: logical AND (&&
) yields true
(since true is the identity for AND), logical OR (||
) yields false
, and the comma operator (,
) yields void()
(Fold expressions (since C++17) - cppreference.com). These are the only operators that can fold an empty pack without an explicitly provided initial value. For all other operators (e.g. arithmetic ones like +
or *
), an empty pack without an init is an error, because the compiler wouldn’t know what value to produce. Thus, if there’s a possibility of no arguments, you must provide an initial value for non-boolean folds (or otherwise ensure at least one argument is present).
Syntax Requirements: Because fold expressions introduce new syntax, there are a few rules to be aware of:
- You must enclose the fold expression in parentheses. For example,
return ... + args;
(without parentheses) is invalid, whereasreturn (... + args);
is correct. - If either the pack expression or the initial expression contains an operator with lower precedence than the fold operator, you should parenthesise that sub-expression to avoid parsing issues (Fold expressions (since C++17) - cppreference.com). For instance, writing
(args + ... + 1 * 2)
is problematic because the multiplication1 * 2
has lower precedence than+
and confuses the parser; it should be written as(args + ... + (1 * 2))
(Fold expressions (since C++17) - cppreference.com). In practice, a good rule is to put parentheses around sub-expressions inside a fold expression unless they are simple variables or literals.
Having covered the syntax and structure of fold expressions, let’s move on to some concrete examples demonstrating how folds can simplify code for various common tasks.
Examples and Use Cases of Fold Expressions
Fold expressions shine in scenarios where a pack of arguments needs to be combined or processed in a uniform way. Below are several illustrative examples using different operators: summing numbers, computing a logical conjunction (testing if all conditions are true), and finding a minimum value. These examples use only the C++ standard library and language features.
Summation of a Parameter Pack
Perhaps the simplest use of a fold is summing a list of values. Without fold expressions, you might implement a sum
function via recursion or overloaded initializer lists. With C++17, it becomes almost trivial:
template<typename... Args>
auto sum(Args... args) {
return (... + args);
}
This single line returns the sum of all values in args...
by folding the +
operator over the parameter pack. It is a unary left fold – equivalent to writing (((arg1 + arg2) + arg3) + ... + argN)
. The C++ Core Guidelines note this approach as good and much more flexible than using C-style varargs or manual recursion (F: Functions – C++). For example, sum(3, 5, 10)
will yield 18
, and sum(3.14, 2.718, 1.0)
will yield approximately 6.858
. The template will work for any types that support the +
operator (and where the + operation is associative enough for summation to make sense).
It’s worth mentioning that if Args...
is empty, the above sum
function would not compile (since there’s nothing to fold and +
has no identity defined in C++). If we wanted sum()
(with no arguments) to return 0, we could provide an initial value: for instance, return (0 + ... + args);
as shown earlier. In most cases, though, one would simply avoid calling sum
with no arguments or handle it separately. The key takeaway is that fold expressions drastically simplify variadic accumulation logic: the compiler generates the cascade of +
operations for us. This improves both clarity and safety, as there’s no recursion or pack-unpacking boilerplate to get wrong.
Logical Conjunction (All True)
Another common scenario is determining if a condition holds for all arguments in a pack (a logical conjunction of values). For example, we might want to implement a function allTrue(x1, x2, ..., xN)
that returns true if every argument is true (or non-zero) and false if any argument is false. Using a fold with the logical AND operator &&
makes this straightforward:
template<typename... Args>
bool allTrue(Args... args) {
return (... && args);
}
This uses a unary left fold of &&
over the pack. It effectively expands to ((arg1 && arg2) && arg3) && ... && argN
. The result is true
only if every arg
in the pack is truthy (convertible to true). If the pack is empty, as mentioned, the fold of &&
yields true
by definition (since an empty conjunction is true – the identity element for logical AND) (Fold expressions (since C++17) - cppreference.com). So allTrue()
with no arguments would return true
(though such a call is probably of limited usefulness).
Let’s see it in action. Suppose we call allTrue(true, true, false, true)
: inside allTrue
, the fold expands to ( (true && true) && false ) && true
(Fold expressions (since C++17) - cppreference.com), which simplifies to (true && false) && true
→ false && true
→ false
. Thus, the function returns false
because not all arguments were true. If we call allTrue(1 < 2, 10, std::strcmp(s1, s2) == 0)
, it will evaluate each expression and combine them with &&
in the same manner. Notably, the fold expression preserves the short-circuit semantics of &&
. The expansion is fully parenthesised, but each &&
will stop evaluating further operands as soon as one false
is encountered, just as in a normal chain of &&
operations. (Likewise, folding with ||
will stop on the first true
it encounters.) This short-circuit behaviour can be useful if checking a series of conditions that might be expensive — you get the benefit of stopping early if a condition fails.
Computing the Minimum Value
Folding is not limited to built-in arithmetic or logical reductions; it can also help with more custom computations. As a more advanced example, let’s find the minimum value in a pack of arguments. C++ does not have a dedicated binary operator for “min”, but we can utilise the standard library and the comma operator fold to achieve this. One approach is:
#include <algorithm> // for std::min
template<typename T, typename... Rest>
T minValue(T head, Rest... tail) {
T minVal = head;
((minVal = std::min(minVal, tail)), ...);
return minVal;
}
Here we take at least one argument (head
) and then a parameter pack of the rest. We initialise a variable minVal
with the first argument. The fold expression ((minVal = std::min(minVal, tail)), ...)
uses the comma operator ,
to expand an assignment operation over each element in tail...
. This is a fold for side effects: it doesn’t directly produce a value (the comma operator’s result is discarded in each step, except the last which we don’t use), but it updates minVal
as a side effect for each argument. After the fold, minVal
holds the smallest value encountered. We then return minVal
. For example, minValue(5, 2, 8, 1, 4)
will return 1
(the minimum). If we call minValue
with only one argument, it simply returns that argument. If we wanted to allow an empty call (no arguments) we would need to provide an initial value and a suitable return type, but in this design we require at least one parameter of type T
.
It’s interesting to note how this works: the fold expands something like:
minVal = std::min(minVal, tail1),
minVal = std::min(minVal, tail2),
...
minVal = std::min(minVal, tailN)
with each comma-separated expression executed in order. The result of each comma expression is the result of the rightmost operation (here the assignment), but we aren’t using the result value; we only care about the accumulated effect on minVal
. This pattern of using a fold with the comma operator is a common idiom to perform an operation for each pack element (such as calling a function for each argument, or updating a state). In our case, it saves us from writing a loop manually. The code is concise yet clear: it initialises minVal
and then folds the operation “update minVal to the smaller of itself and the next element” over all remaining arguments.
Other Use Cases: Fold expressions are versatile. In addition to the above scenarios, some other common uses include:
-
Folding over
,
to execute a function for each argument: e.g., calling a user-provided function on each argument in a pack (similar to applyingstd::initializer_list
with a braced initializer). For instance,(... , (doSomething(args)))
will calldoSomething
on eachargs
. This can replace loops or recursion for pack processing. -
Outputting multiple values: Using the stream insertion operator
<<
in a fold is a neat way to print a sequence of arguments. For example:template<typename... Args> void printAll(Args&&... args) { (std::cout << ... << args) << "\n"; }
This will fold
<<
across all arguments, effectively executingstd::cout << arg1 << arg2 << ... << argN
, followed by a newline (Fold expressions (since C++17) - cppreference.com). This one-liner replaces writing a loop or multiple<<
operations manually. -
Compile-time checks with
constexpr
and traits: You can fold logical operators to assert properties about all types in a pack. For example,static_assert((std::is_integral_v<Args> && ...), "All types must be integral");
will validate at compile time that every type in the parameter packArgs
is an integral type. Similarly, one could use folding with&&
or||
to implement trait checks like “any of the types is trivial”, etc. -
Combining standard library containers or tuples: While more complex, you can imagine folding over
+
or+=
to concatenate strings or accumulate containers, or even usestd::tuple_cat
in a fold expression to combine multiple tuples into one. Fold expressions can make such code much cleaner.
In all these cases, the theme is that fold expressions allow a sequence of operations over a pack to be expressed declaratively in one expression, rather than procedurally in many lines of code. This often leads to code that is shorter and easier to reason about, once you are comfortable with the fold expression syntax.
Best Practices and Common Pitfalls
Fold expressions are powerful, but to use them effectively and safely, consider the following best practices and be mindful of some pitfalls:
-
Prefer fold expressions for simplicity: If you need to aggregate or process all arguments in a pack, a fold expression is usually the simplest and most expressive solution. It avoids the clunky recursion of pre-C++17 variadic templates and clearly communicates the intent to the compiler (which can also optimise the code better). The C++ Core Guidelines exemplify using folds for tasks like summation as a modern best practice (F: Functions – C++).
-
Understand the operator’s identity (neutral element): Before choosing a fold, consider what should happen if the parameter pack is empty. If using a logical operator like
&&
or||
, the language defines the result for an empty pack (true for&&
, false for||
) (Fold expressions (since C++17) - cppreference.com). For other operators, decide on an appropriate identity and use a binary fold with aninit
. For example, for string concatenation you might useinit = ""
(empty string), for multiplication useinit = 1
, and for addition useinit = 0
. Providing an initial value ensures the fold will compile (and produce the identity) even if no arguments are given. -
Choose left vs. right fold appropriately: As demonstrated with subtraction, the outcome can differ based on fold direction if the operator is not associative. Generally, arithmetic and bitwise operations should use left folds (to mimic left-to-right evaluation order as one would naturally write them), unless you have a specific reason for right-associative evaluation. Conversely, some scenarios (like building an expression that naturally nests rightwards) might call for a right fold. Always think through a quick example to verify the fold orientation, especially for operators like
-
,/
,^
(xor), or even function calls where order matters. -
Be mindful of short-circuiting and evaluation order: For
&&
,||
, and,
(comma), the evaluation order is well-defined (left-to-right for&&
and||
, and left-to-right sequencing for comma). A left fold of&&
will evaluate from the first argument to the last, stopping when one is false; a right fold of&&
will start evaluation at the first argument as well (due to how it is parenthesised) and also stop on the first false. In practice, both(args && ... )
and(... && args)
evaluate in left-to-right order because of&&
’s semantics. For most other operators (like+
,*
), C++17 does not guarantee left-to-right evaluation — but since these are usually used for arithmetic without side effects, it shouldn’t matter. If your fold operands have side effects (calling functions, etc.), it’s wise to not rely on a specific order unless using&&
,||
, or,
where the order is guaranteed. In critical cases, refactor to separate the side effects from the fold, or use comma operator folds which do guarantee order. -
Ensure the fold expression is well-parenthesised: Always wrap the entire fold in parentheses, and when in doubt, parenthesise sub-expressions inside the fold. The compiler will complain if you forget the outer parentheses. For inner expressions, as shown earlier, something like
(args + ... + x*y)
can cause a parse error; writing(args + ... + (x*y))
is the correct approach (Fold expressions (since C++17) - cppreference.com). The need arises because the ellipsis has lower precedence than almost everything, and the grammar expects the patternpack op ... op init
with those operators at top level. If aninit
or a part of the pack includes an operator of lower precedence, it must be enclosed in(
)
to be treated as a single expression in that context. -
Avoid overly clever folds that harm readability: While fold expressions can perform astonishing tricks (like the comma-fold trick to retrieve the last element of a pack, or the example to find the minimum using a conditional and comma), not all such code is self-explanatory. Use folds to simplify and clarify code, but if you find yourself writing a very convoluted fold expression, consider if a simple loop or a different approach would be clearer. Modern C++ is about readable and efficient code. A fold expression in itself is quite readable, but chaining multiple different operations in one fold or exploiting short-circuit logic to achieve a side effect (as in some clever one-liners) might confuse readers. When you do use such patterns, add comments or break the steps into separate folds for clarity.
-
Remember that fold expressions only work with binary operators: You cannot directly fold a function like
std::min
or a user-defined functor without using one of the allowed operators as a mediator. If you have a custom operation, you may use a lambda or function call inside a fold via the comma operator (as we did withstd::min
inside the assignment), or consider usingstd::accumulate
or other algorithm if appropriate. In templates, though, folds are often the easiest route as long as you stick to the provided operators.
In summary, stick to the fold expression syntax rules, choose the right form for your needs, and leverage the feature to write cleaner code. Most pitfalls are easy to avoid once you’re aware of them, and the benefits in terms of code brevity and clarity are significant.
Conclusion
Fold expressions are a significant addition in C++17 that elevate the expressiveness of variadic templates. By allowing the automatic expansion of parameter packs with a given operator, they eliminate the need for tedious boilerplate in many scenarios. I have shown how folds can handle summation, logical checks, and even computing a minimum, all with very succinct code. Adopting fold expressions can both simplify your implementations and improve performance (by making the intent clear to the compiler, enabling better optimisations) (From Variadic Templates to Fold Expressions – MC++ BLOG).
From a stylistic perspective, fold expressions encourage a more declarative style of coding in C++ template metaprogramming: you state what reduction to perform, and the language takes care of how to expand it across all arguments. This leads to code that is often closer to the problem statement (e.g., “return the sum of all args”) and avoids the risk of off-by-one errors or recursion mistakes in manual expansions.
As with any powerful feature, it pays to understand the subtleties — such as how left vs. right association works and when to provide an initial value. Once these are mastered, fold expressions become an indispensable tool in the modern C++ toolkit, allowing us to write code that is both elegant and efficient. I encourage you to refactor some of your variadic template code using folds and experience the improvement. The examples and guidelines provided here should serve as a solid foundation for using fold expressions confidently in your own projects.
References
-
C++17 Standard (ISO/IEC 14882:2017) – Fold expressions are defined in section [expr.prim.fold] of the ISO C++17 standard. (See also ISO C++20 [expr.prim.fold] for the same rules.) (Fold expressions (since C++17) - cppreference.com) (Fold expressions (since C++17) - cppreference.com)
-
cppreference.com: “Fold expressions (since C++17)” – an overview of fold expression syntax, explanation, and examples (Fold expressions (since C++17) - cppreference.com) (Fold expressions (since C++17) - cppreference.com).
-
C++ Core Guidelines (F.50) – use of fold expressions for variadic functions, showing a
sum
example as a good practice (F: Functions – C++). -
Grimm, R. (2020). “From Variadic Templates to Fold Expressions”. Modernes C++ Blog. – Discusses how fold expressions replace recursive variadic templates and provides examples like computing
all()
with&&
(From Variadic Templates to Fold Expressions – MC++ BLOG) (From Variadic Templates to Fold Expressions – MC++ BLOG). -
Boccara, J. (2021). “C++ Fold Expressions 101”. Fluent C++. – Introductory article explaining fold expressions with examples, focusing on associativity differences (C++ Fold Expressions 101 - Fluent C++) (C++ Fold Expressions 101 - Fluent C++).