Fold Expressions (C++17) — Unary and Binary Folds, Operator Packs, Compile-Time Reduction
In this tutorial, you will learn about Fold Expressions (C++17). We cover key concepts, practical examples, and best practices to help you master this topic.
C++17 fold expressions apply binary operators directly to parameter packs — (args + ...), (... && args), (args || ...) — eliminating recursive template expansion for compile-time reductions.
What You'll Learn
You will write unary and binary fold expressions for all 32 foldable operators, understand left-fold vs right-fold evaluation order, use fold expressions with logical operators for compile-time checks, combine folds with comma operator for sequential operations, and apply folds in real-world patterns like Type Checking and container operations.
Why It Matters
Before fold expressions, reducing a parameter pack to a single value required recursive templates — verbose, error-prone, and slow to compile. A fold expression does the same work in one line. They are essential in variadic template code, powering everything from std::apply to custom tuple implementations. C++ libraries rely on folds for concise, high-performance variadic operations.
Learning Path
graph LR
A["55: if/switch init + if constexpr"] --> B["56: Fold Expressions"]
B --> C["57: Coroutines"]
C --> D["58: Modules"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
Unary Fold Syntax
There are four unary fold forms:
| Form | Syntax | Expansion (for args = a, b, c) |
|---|---|---|
| Right fold | (args op ...) |
(a op (b op c)) |
| Left fold | (... op args) |
((a op b) op c) |
| Right fold (empty) | (args op ...) |
Error with empty pack if op requires value |
| Left fold (empty) | (... op args) |
Error with empty pack if op requires value |
#include <iostream>
#include <string>
// Unary right fold: (args + ...) → (a + (b + (c + ...)))
template <typename... Args>
auto sumRight(Args... args) {
return (args + ...);
}
// Unary left fold: (... + args) → (((a + b) + c) + ...)
template <typename... Args>
auto sumLeft(Args... args) {
return (... + args);
}
int main() {
// For + (associative), both produce the same result
std::cout << sumRight(1, 2, 3, 4, 5) << "\n"; // 15
std::cout << sumLeft(1, 2, 3, 4, 5) << "\n"; // 15
// For non-associative operators, order matters
// Right fold: (1 - (2 - (3 - 4))) = 1 - (2 - (3 - 4))
// = 1 - (2 - (-1))
// = 1 - 3 = -2
// Left fold: (((1 - 2) - 3) - 4) = -8
}
Binary Fold Syntax
Binary folds provide an identity value for empty packs:
| Form | Syntax | Expansion |
|---|---|---|
| Right binary | (args op ... op init) |
(a op (b op (c op init))) |
| Left binary | (init op ... op args) |
(((init op a) op b) op c) |
#include <iostream>
#include <string>
// Binary fold with identity value
template <typename... Args>
auto sumWithDefault(Args... args) {
return (args + ... + 0); // 0 is the identity for addition
}
// String concatenation with identity
template <typename... Args>
std::string concat(Args... args) {
return (args + ... + std::string{}); // Empty string identity
}
int main() {
// With binary fold, empty pack is valid
std::cout << sumWithDefault() << "\n"; // 0 (empty pack)
std::cout << sumWithDefault(1, 2, 3) << "\n"; // 6
std::cout << concat() << "\n"; // (empty string)
std::cout << concat("hello", " ", "world") << "\n"; // hello world
// Multiplication with identity 1
auto product = [](auto... args) {
return (args * ... * 1);
};
std::cout << product() << "\n"; // 1
std::cout << product(2, 3, 4) << "\n"; // 24
}
Logical Fold Expressions
Logical folds (&&, ||, ,) with empty packs have well-defined behavior: && returns true, || returns false, and , returns void().
#include <iostream>
#include <type_traits>
#include <concepts>
// All true?
template <typename... Args>
constexpr bool allTrue(Args... args) {
return (... && args); // Empty pack → true
}
// Any true?
template <typename... Args>
constexpr bool anyTrue(Args... args) {
return (... || args); // Empty pack → false
}
// Compile-time type check all same
template <typename T, typename... Args>
constexpr bool allSame() {
return (std::is_same_v<T, Args> && ...);
}
// Check if all types are integral
template <typename... Args>
constexpr bool allIntegral() {
return (std::is_integral_v<Args> && ...);
}
int main() {
std::cout << std::boolalpha;
std::cout << "allTrue: " << allTrue(true, true, true) << "\n"; // true
std::cout << "allTrue (false): " << allTrue(true, false) << "\n"; // false
std::cout << "allTrue (empty): " << allTrue() << "\n"; // true
std::cout << "anyTrue: " << anyTrue(false, true, false) << "\n"; // true
std::cout << "anyTrue (all false): " << anyTrue(false, false) << "\n"; // false
std::cout << "anyTrue (empty): " << anyTrue() << "\n"; // false
std::cout << "allSame<int, int, int>: " << allSame<int, int, int>() << "\n"; // true
std::cout << "allSame<int, double>: " << allSame<int, double>() << "\n"; // false
std::cout << "allIntegral<int, long, char>: "
<< allIntegral<int, long, char>() << "\n"; // true
std::cout << "allIntegral<int, double>: "
<< allIntegral<int, double>() << "\n"; // false
}
Comma Fold for Sequential Operations
The comma operator fold executes each expression in sequence.
#include <iostream>
#include <vector>
// Print all arguments (left fold with comma)
template <typename... Args>
void printAll(const Args&... args) {
((std::cout << args << " "), ...); // Comma operator: (a, b, c) → executes each
std::cout << "\n";
}
// Push all into a vector
template <typename T, typename... Args>
void pushAll(std::vector<T>& vec, Args&&... args) {
(vec.push_back(std::forward<Args>(args)), ...);
}
// Call a function for each argument
template <typename Func, typename... Args>
void forEach(Func f, Args&&... args) {
(f(std::forward<Args>(args)), ...);
}
int main() {
printAll(1, 2, 3, "hello", 3.14);
// 1 2 3 hello 3.14
std::vector<int> v;
pushAll(v, 1, 2, 3, 4, 5);
for (int x : v) std::cout << x << " ";
std::cout << "\n"; // 1 2 3 4 5
forEach([](auto x) { std::cout << x * 2 << " "; }, 1, 2, 3, 4);
std::cout << "\n"; // 2 4 6 8
}
Fold with Custom Operators
Classes can override operators used in fold expressions.
#include <iostream>
#include <string>
#include <vector>
struct Data {
std::vector<int> values;
};
Data& operator+=(Data& a, const Data& b) {
a.values.insert(a.values.end(), b.values.begin(), b.values.end());
return a;
}
Data operator+(Data a, const Data& b) {
a += b;
return a;
}
// Fold with custom += operator
template <typename... Args>
Data mergeData(Args&&... args) {
Data result;
(result += ... += std::forward<Args>(args)); // ((result += a) += b) += c
return result;
}
int main() {
Data d1{{1, 2}};
Data d2{{3, 4}};
Data d3{{5, 6}};
Data merged = mergeData(d1, d2, d3);
for (int v : merged.values) std::cout << v << " ";
std::cout << "\n"; // 1 2 3 4 5 6
}
Fold in constexpr Context
Fold expressions in constexpr functions compile to compact code.
#include <iostream>
#include <array>
// Compile-time sum of parameter pack
template <int... Values>
struct CompileTimeSum {
static constexpr int value = (Values + ...);
};
// Compile-time max (using ternary operator in fold)
template <typename... Args>
constexpr auto maxFold(const Args&... args) {
static_assert(sizeof...(args) > 0, "max requires at least one argument");
return ((args > ...) // This doesn't work for max
// Use initializer list approach:
std::max({args...}));
}
// Better compile-time max with fold
template <typename T, typename... Args>
constexpr T maxFold2(T first, Args... rest) {
T result = first;
((result = (rest > result) ? rest : result), ...);
return result;
}
int main() {
std::cout << CompileTimeSum<1, 2, 3, 4, 5>::value << "\n"; // 15
constexpr int m1 = maxFold2(10, 5, 20, 8, 15);
std::cout << "max: " << m1 << "\n"; // 20
constexpr int m2 = maxFold2(-5, -2, -10);
std::cout << "max (negatives): " << m2 << "\n"; // -2
}
Common Mistakes
Mistake 1: Using wrong operator precedence in fold
// Fold has parsing issues with some operators
auto r = (args << ...); // OK: ((a << b) << c)
auto r2 = ... << args; // OK: (a << (b << c))
Know the precedence: << is left-to-right, so (args << ...) is left fold.
Mistake 2: Unary fold with empty pack and no identity
auto sum() { return (args + ...); } // Error if Args is empty
Use binary fold (args + ... + 0) for empty-safe operations.
Mistake 3: Using fold with operator that has no identity
// No identity for / operator
auto divide(auto... args) { return (args / ...); } // Fails for empty pack
Some operators have no identity. Use static_assert(sizeof...(args) > 0).
Mistake 4: Forgetting that comma fold evaluates left to right
((f(args), g(args)), ...); // f(a), g(a), f(b), g(b), ...
Each element's expression is fully evaluated before the next.
Mistake 5: Using fold with assignment operators incorrectly
(result += ... += args); // OK: binary left fold with +=
Assignment operators have right-to-left associativity, so the fold must match.
Practice Questions
- What is the output?
std::cout << (true && ... && std::array{true, false, true});
Answer: false — the fold expands to true && false && true = false.
What does
(args + ... + 0)do whenargsis empty? Answer: Returns0(the identity value). The binary fold provides a default for empty packs.Write a fold expression that checks if a value equals any of the arguments.
template <typename T, typename... Args>
bool equalsAny(const T& value, const Args&... args) {
return ((value == args) || ...);
}
What is the difference between
(... + args)and(args + ...)? Answer:(... + args)is left fold(((a+b)+c)+d).(args + ...)is right fold(a+(b+(c+d))). For associative operators like +, they produce the same result.Can fold expressions work with the ternary
?:operator? Answer: No. The ternary operator is not a foldable operator. Usestd::max({args...})or expand manually.
FAQ
Mini Project
Implement a CSV formatter using fold expressions that joins any number of values with a delimiter:
#include <iostream>
#include <string>
#include <sstream>
// Your csv format function using fold
int main() {
std::cout << csv(", ", 1, 2, 3) << "\n"; // 1, 2, 3
std::cout << csv(", ", "apple", "banana") << "\n"; // apple, banana
std::cout << csv(", ", 3.14, "text", 42) << "\n"; // 3.14, text, 42
std::cout << csv(", ") << "\n"; // (empty string)
// Different delimiter
std::cout << csv("|", 10, 20, 30) << "\n"; // 10|20|30
}
This project demonstrates how C++ fold expressions create concise, type-safe variadic functions. Compare with Python's str.join() which only works with strings — the C++ version is generic across all types.
What's Next
You now master fold expressions — the most concise tool for variadic operations. Next, you will learn coroutines (C++20), which enable cooperative multitasking with suspend/resume semantics.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro