Why C++17 Fold Expressions Turn Variadic Templates into One‑Liners
This article explains how C++17 fold expressions replace verbose recursive variadic template tricks with concise one‑line constructs, covering their four syntactic forms, practical examples such as summing, printing, compile‑time checks, and common pitfalls to avoid.
Variadic templates introduced in C++11 required recursive overloads to process a parameter pack, which added boilerplate code and specializations for empty packs. A classic C++11/14 sum implementation looks like:
template<typename T> T sum(T t) { return t; }
template<typename T, typename... Args> T sum(T first, Args... rest) { return first + sum(rest...); }Calling sum() also required an extra overload or SFINAE constraints.
Fold Expressions in C++17
C++17 introduces fold expressions, allowing the same operation to be expressed in a single line without recursion. The generic sum becomes:
template<typename... Args> auto sum(Args... args) { return (args + ...); }This is a unary right fold that the compiler expands to a nested addition expression at compile time.
Four Forms of Fold Expressions
Unary right fold : (args op ...) expands to a op (b op (c ...)).
Unary left fold : (... op args) expands to ((a op b) op c) ....
Binary right fold : (args op ... op init) expands to a op (b op (c op init)).
Binary left fold : (init op ... op args) expands to ((init op a) op b) op c ....
The operator op can be any binary operator supported by C++ (e.g., +, -, *, /, %, &&, ||, ,, <<, >>, ==, etc.). In total 32 operators are fold‑compatible.
Practical Comparisons
1. Summing a Parameter Pack
C++11/14 (recursive) :
template<typename T> T sum(T t) { return t; }
template<typename T, typename... Args> T sum(T first, Args... rest) { return first + sum(rest...); }
int result = sum(1, 2, 3, 4, 5); // 15To support an empty call sum() a separate overload is required.
C++17 (fold) :
template<typename... Args> auto sum(Args... args) { return (args + ...); }
int result = sum(1, 2, 3, 4, 5); // 15Adding an initializer handles the empty pack:
template<typename... Args> auto sum(Args... args) { return (args + ... + 0); }2. Printing a Parameter Pack
C++11/14 uses recursion and manual spacing:
template<typename T> void print(T t) { std::cout << t << std::endl; }
template<typename T, typename... Args> void print(T first, Args... rest) {
std::cout << first << " ";
print(rest...);
}
print(1, "hello", 3.14); // 1 hello 3.14C++17 leverages a binary left fold with the stream insertion operator:
template<typename... Args> void print(Args... args) {
(std::cout << ... << args) << std::endl;
}To insert spaces, a comma fold can be used:
template<typename... Args> void print(Args... args) {
((std::cout << args << " "), ...);
std::cout << std::endl;
}3. Compile‑time All‑Integral Check
C++11/14 requires a recursive trait:
template<typename T> constexpr bool all_integral() { return std::is_integral_v<T>; }
template<typename T, typename U, typename... Rest> constexpr bool all_integral() {
return std::is_integral_v<T> && all_integral<U, Rest...>();
}
static_assert(all_integral<int, short, long>(), "All args must be integral");C++17 collapses the check into a single static_assert using a unary right fold:
template<typename... Args> void process(Args... args) {
static_assert((std::is_integral_v<Args> && ...), "All args must be integral");
// ...
}4. Any‑Floating‑Point Check (using || )
template<typename... Args> constexpr bool any_floating_point() {
return (std::is_floating_point_v<Args> || ...);
}5. Applying an Action to Every Argument (Comma Fold)
Push a series of values into a std::vector:
template<typename T, typename... Args> void push_all(std::vector<T>& vec, Args&&... args) {
(vec.push_back(std::forward<Args>(args)), ...);
}Enable a set of widgets:
template<typename... Widgets> void enable_all(Widgets&... widgets) {
(widgets.enable(), ...);
}Call a method from multiple base classes:
template<typename... Bases> struct MultiBase : Bases... {
template<typename... Args> MultiBase(Args&&... args) : Bases(std::forward<Args>(args))... {}
void call_all() { (Bases::do_something(), ...); }
};Common Pitfalls
Empty pack with unary folds triggers a compilation error; use a binary fold with an initializer (e.g., (args + ... + 0)).
Operator precedence : parenthesize inner expressions when mixing operators, e.g., ((args * 2) + ...).
Evaluation order : logical &&, ||, and the comma operator guarantee left‑to‑right evaluation. Arithmetic operators ( +, *, etc.) do not guarantee order, which matters only if the operands have side effects.
When not to use folds : if different arguments need distinct handling, recursion, if constexpr, or std::apply remain appropriate.
Conclusion
Fold expressions are one of the highest‑ROI features of C++17: they dramatically shrink code, make intent clear, and eliminate the boilerplate of recursive variadic templates. Projects already using C++17 should adopt them, and teams on older standards can use this as a compelling reason to upgrade their compiler.
IT Services Circle
Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
