Fundamentals 14 min read

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.

IT Services Circle
IT Services Circle
IT Services Circle
Why C++17 Fold Expressions Turn Variadic Templates into One‑Liners

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); // 15

To 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); // 15

Adding 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.14

C++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.

Ccode optimizationTemplate MetaprogrammingC++17Fold ExpressionsVariadic Templates
IT Services Circle
Written by

IT Services Circle

Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.