Generic Folding Without C++17 Fold Expressions
Posted
After commenting on Baptiste Wicht’s blog post on C++17 fold expressions (this post is an extension of my comment) and reading N4295, I was left wondering what purpose they fulfill.
C++17 fold expressions exist in four forms (although the last two are syntactically identical, the only difference being which expression is an unexpanded parameter pack):
template<typename... Args>
auto fold_expressions(Args&&... args)
{
(... + args); // unary left fold, corresponds to ((args_1 + args_2) + ...) + args_n
(args + ...); // unary right fold, corresponds to args_1 + (... + (args_n-1 + args_n))
(1 + ... + args); // binary left fold, same as unary but starts with 1
(args + ... + 1); // binary right fold, same as unary but ending with 1
}
This is certainly cleaner than writing a recursive function each and every time we want to fold a parameter pack using a binary operator. Nice, right? But what about other binary functions? Why limit a fold only to fixed operators? Sure, you could write a wrapper that defines a binary operator as the application of an arbitrary function as this Stack Overflow answer suggests, but that seems less than clean. Another thing that bothers me, is that the syntax does not directly suggest if a left or right fold is performed.
Let’s approach this from another direction. What can you use right now with a C++14 compiler? By using automatic return type deduction and passing a functor, it is fairly easy to write generic left and right fold functions:
#include <utility>
template<typename BinaryFunctor, typename Arg>
constexpr auto foldr(BinaryFunctor, Arg && arg)
{
return std::forward<Arg>(arg);
}
template<typename BinaryFunctor, typename Arg0, typename... Args>
constexpr auto foldr(BinaryFunctor f, Arg0 && arg0, Args &&... args)
{
return f(std::forward<Arg0>(arg0), foldr(f, std::forward<Args>(args)...));
}
template<typename BinaryFunctor, typename Arg>
constexpr auto foldl(BinaryFunctor, Arg && arg)
{
return std::forward<Arg>(arg);
}
template<typename BinaryFunctor, typename Arg0, typename Arg1, typename... Args>
constexpr auto foldl(BinaryFunctor f, Arg0 && arg0, Arg1 && arg1, Args &&... args)
{
return foldl(f, f(std::forward<Arg0>(arg0), std::forward<Arg1>(arg1)), std::forward<Args>(args)...);
}
This might seem to complicate usage with simple operators but <functional>
already offers generic functors for most operators (except shifts and augmented assignments), so the equivalent to the first block of code would be:
template<typename... Args>
auto fold_functions(Args&&... args)
{
foldl(std::plus<>{}, args...);
foldr(std::plus<>{}, args...);
foldl(std::plus<>{}, 1, args...);
foldr(std::plus<>{}, args..., 1);
}
There are a couple of possibilities of how this could be extended, for example a neutral_value<Functor>
type trait or template variable could be introduced to allow for folds of empty parameter packs (such as fold expressions allow for some operators).
There might be some use cases in Concepts as Kerrek SB suggested, but it is unclear to me which ones these would be. I could not find anything in the corresponding papers.
Does this mean I am opposed to fold expressions? Not necessarily. There’s nothing wrong with a little syntactic sugar. Range based for loops could also have been implemented using only lambda and a template function. constexpr
functions can often be replaced with template metaprogramming, but using constexpr
functions is usually easier on the compiler and results in shorter compile times.