Jonathan Boccara's blog

What C++ Fold Expressions Can Bring to Your Code

Published March 19, 2021 - 0 Comments

Daily C++

In the previous post we saw how fold expressions work. We saw how to define them, how to control their associativity, and how to handle empty variadic packs in a fold expression.

But all along we’ve been using an example that didn’t bring much value to code: a function that makes the sum of its parameters:

template<typename... Values>
auto sum(Values const&... values)
{
    return (0 + ... + values);
}

It can be called like this:

sum(1, 2, 3, 4);

But this isn’t a very useful example, as we can just as well write this expression ourselves:

1 + 2 + 3 + 4

Now that we’re clear on how fold expressions work, let’s see examples where they can make our code more expressive.

Doing work in the function

If we do some work on the parameters, the fold expression becomes useful. For instance, we can make a function that computes the average of its inputs:

template<typename... Values>
auto average(Values const&... values)
{
    constexpr auto numberOfValues = double{sizeof...(values)};
    static_assert(numberOfValues > 0);
    return (... + values) / numberOfValues;
}

In this case, we don’t support the case where no arguments is passed, because the average of nothing doesn’t mean anything.

Or, to support bigger numbers, we can divide them by the number of inputs before adding them:

template<typename... Values>
auto average(Values const&... values)
{
    constexpr auto numberOfValues = double{sizeof...(values)};
    static_assert(numberOfValues > 0);
    return (... + (values / numberOfValues));
}

Another way to prevent the function from being called with no parameters is to extract one parameter from the pack:

template<typename Value, typename... Values>
auto average(Value const& value, Values const&... values)
{
    return (value + ... + values) / (1. + sizeof...(values));
}

In this case the version allowing bigger numbers becomes this:

template<typename Value, typename... Values>
auto average(Value const& value, Values const&... values)
{
    return ((value / (1. + sizeof...(values))) + ... + (values / (1. + sizeof...(values))));
}

Repeating operations

A common usage example of fold expressions is to fold over the comma operator.

As a reminder, the default version of the comma operator executes the left operand, then the right operand, then returns the right operand.

For example, if the comma operator is not overloaded, then this expression:

f(x), g(y)

does the following three things in this order:

  • call f(x),
  • call g(y),
  • returns g(y).

The comma operator can executes two operations. Therefore folding over the comma operator can execute an arbitrary number of expressions.

This is useful on a variety of examples.

Adding several elements to a vector

A first example is to add elements to an existing std::vector. To do this, we need to repeat individual push_backs:

auto v = std::vector<int>{1, 2, 3};

// ...

v.push_back(4);
v.push_back(5);
v.push_back(6);
v.push_back(7);
v.push_back(8);
v.push_back(9);
v.push_back(10);

By folding over the comma operator, we can add all those values in a single expression:

template<typename T, typename... Ts>
void push_back(std::vector<T>& v, Ts&&... values)
{
    (v.push_back(std::forward<Ts>(values)), ...);
}

We can then call this function this way:

push_back(v, 4, 5, 6, 7, 8, 9, 10);

Let’s pause a moment to consider associativity. The associativity is on the side of the dot dot dot. So won’t the values be push_back‘d in reverse order? Shouldn’t we rather write this instead:

template<typename T, typename... Ts>
void push_back(std::vector<T>& v, Ts&&... values)
{
    (..., v.push_back(std::forward<Ts>(values)));
}

It turns out that both expressions lead to the same result. To simplify the example, let’s consider a call with only three values:

push_back(v, 4, 5, 6);

With our first version of push_back, the fold expression resolves to this:

v.push_back(4), (v.push_back(5), v.push_back(6));

As a left argument, the push_back(4) gets executed first. We can therefore keep the first version, that looks better in my opinion.

Note that another way to go about this particular example is to use Boost Assign.

Calling a function on the parameters of a function

We can generalise the above example. With fold expressions, we can easily call a function on the parameters of a function:

template<typename Function, typename... Values>
auto for_each_arg(Function function, Values&&... values)
{
    return (function(std::forward<Values>(values)),...);
}

Then calling push_back is a special case of this function:

for_each_arg([&v](auto&& value){ v.push_back(value); }, 4, 5, 6, 7, 8, 9, 10);

Although have a dedicated push_back function arguably led to a more expressive call site.

It is possible implement for_each_arg in C++11 without fold expressions, but it is much less straightforward.

The overloaded pattern

In Functors are not dead, we saw that we sometimes need to bundle several functions into one object:

struct CompareWithId
{
    bool operator()(Employee const& employee, int id)
    {
        return employee.getId() < id;
    }
    bool operator()(int id, Employee const& employee)
    {
        return id < employee.getId();
    }
};

For example, that could be useful to create a custom comparator for algorithms such as std::set_difference.

How can we bundle those two functions into a lambda? Before C++17, it’s complicated.

But with fold expressions, we can implement the so-called “overloaded” pattern.

The overloaded pattern consists in this strange structure:

template<typename... Lambdas>
struct overloaded : public Lambdas...
{
    explicit overloaded(Lambdas... lambdas) : Lambdas(lambdas)... {}

    using Lambdas::operator()...;
};

This structure inherits from several lambdas, can be constructed from those lambdas, and folds over the using expression.

This fold expression allows to import all the operator() from the lambda base classes.

A variation in the implementation of overloaded is to replace the constructor by a deduction guide, to benefit from C++17 template deduction types in constructor:

template<typename... Lambdas>
struct overloaded : public Lambdas...
{
    using Lambdas::operator()...;
};

template<typename... Lambdas> overloaded(Lambdas...) -> overloaded<Lambdas...>;

Either way, with overloaded we can instantiate an object that bundles several lambdas:

auto compareWithId = overloaded
{
    [](auto&& employee, int id) { return employee.getId() < id; },
    [](int id, auto&& employee) { return id < employee.getId();}
};

Calling it will call the corresponding lambda. For example:

compareWithId(employee, id);

calls the first lambda, and

compareWithId(id, employee);

calls the second lambda.

Fold expressions may be the final nail in the coffin of old explicitly declared function objects.

Do you have other use cases for fold expressions?

How do you use them to make your code more expressive?

You will also like

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin