C++ folding expressions

Modern C++ has support for passing parameter packs to functions. What may not be apparent is that in addition to forwarding them to other functions, or recursively to the same function, they can be manipulated in a type-safe manner by the same operation applied to each one in turn. The concept of folding (as this application is known) may be familiar to programmers coming from a functional programming background, or to those with a background in Math.

In C++, folding expressions come in left and right forms, both unary and binary. They all operate on a parameter pack, with the expansion being implied by the use of an ellipsis (...). The binary left form can be demonstrated using std::cout inside a variadic function:

// cout_fold.cpp : use of output stream with folding expression

#include <iostream>

using namespace std;

template<typename... Ts>
void print_all(Ts&&... args) {
    (cout << ... << args);
}

int main() {
    print_all(42, "hello", 1.23, '\n');
}

Note that the parentheses are necessary. In fact, this could instead be written as an unary fold in the following ways (note the use of operator , which is named the “sequencing operator”):

    (..., (cout << args));  // unary left fold
    ((cout << args), ...);  // unary right fold

(Determining which produces more efficient code is left as an exercise to the reader.)

Typically, a unary fold produces a result which can be assigned to a (single) variable, while a binary (most usually left) fold applies each element of the parameter pack to an object in turn. The following program calculates the sum and product of a sequence of integers using all four types of folding expression. Note that the first four folding expressions each yield a result which is assigned to a variable, while the final two modify a variable in-place. Also, note that std::make_index_sequence generates a zero-based sequence of size_t, the template non-type parameter Zero is used to discard this (otherwise the products could not be calculated in this way):

// fold_seq.cpp : use folding expressions to calculate sum and product of a number sequence

#include <iostream>
#include <utility>

using namespace std;

template<size_t Zero, size_t... Numbers>
void sum_product(index_sequence<Zero, Numbers...> seq)
{
    cout << "Numbers: ";
    ((cout << Numbers << ' '), ...);
    cout << '\n';
    
    auto s1 = (Numbers + ...);      // unary right fold
    auto p1 = (... * Numbers);      // unary left fold
    auto s2 = (Numbers + ... + 0);  // binary right fold
    auto p2 = (1 * ... * Numbers);  // binary left fold
    
    auto s3{ 0 }, p3{ 1 };
    (s3 += ... += Numbers);         // binary left folds using
    (p3 *= ... *= Numbers);         // right-associative operators
    
    cout << "Sum: " << s1 << ", " << s2 << ", " << s3 << '\n';
    cout << "Product: " << p1 << ", " << p2 << ", " << p3 << '\n';
}

int main() {
    using numbers = make_index_sequence<6>; // 0..5
    sum_product(numbers{});
}

To finish off, here is a more complete program operating on streams, which uses an unary right fold to output the values of three user-entered variables (separated by commas), and a binary left fold to get these values. You might think that a for-loop could always be used to to this, but this is only possible if the variables are of the same type. Notice the use of perfect forwarding which allows complex objects to be put to std::cout in the most efficient way, and the fact that the (modified) streams are returned by reference (in the style of an overloaded operator << and similar):

// fold_demo.cpp : get and print three variables of different types

#include <iostream>
#include <string>

using namespace std;

template<typename T, typename... Ts>
ostream& print_all(ostream& out, T&& arg0, Ts&&... args) {
    out << forward<T>(arg0);
    if (sizeof...(args) > 0) {
        ((out << ", " << forward<Ts>(args)), ...);
    }
    return out;
}

template<typename... Ts>
istream& input_all(istream& in, Ts&... args) {
    return (in >> ... >> args);
}

int main() {
    cout << "Please enter an integer, a single word and a floating-point number, separated by a space:\n";
    int i; string s; double d;
    input_all(cin, i, s, d);
    cout << "You entered: \"";
    print_all(cout, i, s, d);
    cout << "\"\n";
}

Sample output from running this program:

Please enter an integer, a single word and a floating-point number, separated by a space:
21 many 3.14
You entered: "21, many, 3.14"

To summarize, the different fold expressions are:

(... op pack);  // unary left fold
(pack op ...);  // unary right fold
(obj op ... op pack);  // binary left fold
(pack op ... op obj);  // binary right fold

Here pack is the unexpanded parameter pack, op is almost any (overloadable) operator in C++, and obj is a value, variable or object for which op is defined (obj is sometimes referred to as init as it is allowed to have initial state to provide to the binary folding expression).

Resources: Download or browse the source code

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s