C++ Fold Expressions | Folding Parameter Packs in C++17
이 글의 핵심
Fold expressions let you apply an operator across a parameter pack in one expression—often replacing recursive templates.
What are fold expressions?
C++17 fold expressions “fold” a variadic template parameter pack with a single operator, without writing recursive templates. After you know template basics, folds make many variadic utilities shorter.
// Before C++17: recursion
template<typename T>
T sum(T value) {
return value;
}
template<typename T, typename... Args>
T sum(T first, Args... args) {
return first + sum(args...);
}
// C++17: fold expression
template<typename... Args>
auto sum(Args... args) {
return (... + args); // unary left fold
}
cout << sum(1, 2, 3, 4, 5) << endl; // 15
The four fold forms
// 1. Unary right fold: (args op ...)
template<typename... Args>
auto sum1(Args... args) {
return (args + ...); // ((1 + 2) + 3) + 4
}
// 2. Unary left fold: (... op args)
template<typename... Args>
auto sum2(Args... args) {
return (... + args); // 1 + (2 + (3 + 4))
}
// 3. Binary right fold: (args op ... op init)
template<typename... Args>
auto sum3(Args... args) {
return (args + ... + 0); // ((1 + 2) + 3) + 0
}
// 4. Binary left fold: (init op ... op args)
template<typename... Args>
auto sum4(Args... args) {
return (0 + ... + args); // 0 + (1 + (2 + 3))
}
Unary vs binary folds
- Unary fold: The pack appears on one side of the operator only. For an empty pack, only certain operators are allowed (see below); others are ill-formed.
- Binary fold:
(pack op ... op init)or(init op ... op pack)supplies an initializer, so an empty pack can yieldinit—handy for sums, products, and logical folds.
Same + operator: (0 + ... + args) starts from 0; (... + args) folds only the arguments. Associativity and empty-pack behavior differ.
Left vs right fold
- Left fold
(... op args): grouping associates from the left (exact tree follows the standard’s expansion rules). - Right fold
(args op ...): grouping associates from the right.
For associative operations (+, *), order often does not matter for the value; for subtraction, division, or stream insertion, you must pick the fold that matches the intended order (e.g. (cout << ... << args) for natural print order).
Practical use in variadic functions
Typical patterns: (1) apply one operation to every argument; (2) split “first vs rest.” Folds excel at (1).
template<typename... Args>
void invoke_all(Args&&... f) {
(std::forward<Args>(f)(), ...);
}
If zero arguments are possible, prefer a binary fold with an initializer or if constexpr (sizeof...(Args) == 0).
Common patterns: sum, print, all_of
| Goal | Idiomatic fold | Notes |
|---|---|---|
| Sum | (0 + ... + args) or (... + args) | Use binary fold if empty is allowed |
| Product | (1 * ... * args) | Same idea |
| All true | (... && args) | Empty pack ⇒ true (unary && rules) |
| Any true | (false || ... || args) or (args || ...) | Empty unary || ⇒ false |
(std::cout << ... << args) | Mind stream types and order | |
push_back | (vec.push_back(args), ...) | Comma fold |
Logical OR can use a binary fold with a false initializer or a unary right fold; for “all arguments are true,” (... && args) is often the clearest.
Role in the language
Fold expressions are a pure compile-time syntactic extension in C++17. There is no runtime cost for the fold itself; generated code is usually a flat sequence of operations. With C++20 concepts, you can constrain which types may appear in a fold.
Supported operators
Arithmetic, logical, bitwise, comparison, and comma operators are supported as specified in the standard—see cppreference for the full list.
Examples
template<typename... Args>
void print(Args... args) {
(cout << ... << args) << endl;
}
print("x = ", 42, ", y = ", 3.14);
All true
template<typename... Args>
bool all(Args... args) {
return (... && args);
}
Push into a vector
template<typename T, typename... Args>
void push_back_all(vector<T>& vec, Args... args) {
(vec.push_back(args), ...);
}
Call functions
template<typename... Funcs>
void call_all(Funcs... funcs) {
(funcs(), ...);
}
Range / membership checks
Illustrative patterns—adapt to your types.
Min/max
Using std::min with an initializer list is often clearer than misusing < as a fold:
template<typename... Args>
auto minimum(Args... args) {
return std::min({args...});
}
Comma operator
template<typename... Args>
void process2(Args... args) {
((cout << args << " "), ...);
}
Common pitfalls
Empty packs
template<typename... Args>
auto sum(Args... args) {
return (0 + ... + args); // OK: binary fold
}
// Empty-pack friendly unary folds: &&, ||, ,
Precedence
template<typename... Args>
auto func(Args... args) {
return (... + (args * 2)); // parenthesize
}
Type consistency
template<typename T, typename... Args>
T sum(Args... args) {
return (T(0) + ... + T(args));
}
Fold vs recursion
Folds replace long recursive templates for many “reduce over pack” patterns; recursion is still useful when you need non-uniform structure.
FAQ
Q1: When should I use folds?
A: Variadic templates, pack processing, and anywhere you would otherwise write recursion.
Q2: Are all operators supported?
A: Binary operators yes; unary operators cannot be folded in the same way.
Q3: Performance?
A: Similar to an equivalent expanded loop; compile time may improve vs deep recursion.
Q4: Empty packs?
A: Only &&, ||, , unary folds allow an empty pack; otherwise use a binary fold or a branch.
Q5: Before C++17?
A: Use recursive templates or helper metafunctions.
Q6: Resources?
A: C++17 — The Complete Guide, cppreference, Effective Modern C++.
Related: Advanced variadic templates, template basics, template argument deduction.
Related posts
- Advanced variadic templates
- Template basics
- Template argument deduction
Keywords
C++, fold expression, variadic, parameter pack, C++17.
See also
- Variadic templates
autodeduction- CTAD
- C++20 Concepts
constexpr if