Generic Folding Without C++17 Fold Expressions

Af­ter com­ment­ing on Bap­tiste Wicht’s blog post on C++17 fold ex­pres­sions (this post is an ex­ten­sion of my com­ment) and read­ing N4295, I was left won­der­ing what pur­pose they ful­fill.

C++17 fold ex­pres­sions ex­ist in four forms (al­though the last two are syn­tac­ti­cal­ly iden­ti­cal, the on­ly dif­fer­ence be­ing which ex­pres­sion is an un­ex­pand­ed pa­ram­e­ter 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 cer­tain­ly clean­er than writ­ing a re­cur­sive func­tion each and ev­ery time we want to fold a pa­ram­e­ter pack us­ing a bi­na­ry op­er­a­tor. Nice, right? But what about oth­er bi­na­ry func­tions? Why lim­it a fold on­ly to fixed op­er­a­tors? Sure, you could write a wrap­per that de­fines a bi­na­ry op­er­a­tor as the ap­pli­ca­tion of an ar­bi­trary func­tion as this Stack Over­flow an­swer sug­gests, but that seems less than clean. An­oth­er thing that both­ers me, is that the syn­tax does not di­rect­ly sug­gest if a left or right fold is per­formed.

Let’s ap­proach this from an­oth­er di­rec­tion. What can you use right now with a C++14 com­pil­er? By us­ing au­to­mat­ic re­turn type de­duc­tion and pass­ing a func­tor, it is fair­ly easy to write gener­ic left and right fold func­tions:

#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 com­pli­cate us­age with sim­ple op­er­a­tors but <functional> al­ready of­fers gener­ic func­tors for most op­er­a­tors (ex­cept shifts and aug­ment­ed as­sign­ments), so the equiv­a­lent 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 cou­ple of pos­si­bil­i­ties of how this could be ex­tend­ed, for ex­am­ple a neutral_value<Functor> type trait or tem­plate vari­able could be in­tro­duced to al­low for folds of emp­ty pa­ram­e­ter packs (such as fold ex­pres­sions al­low for some op­er­a­tors).

There might be some use cas­es in Con­cepts as Ker­rek SB sug­gest­ed, but it is un­clear to me which ones these would be. I could not find any­thing in the cor­re­spond­ing pa­pers.

Does this mean I am op­posed to fold ex­pres­sions? Not nec­es­sar­i­ly. There’s noth­ing wrong with a lit­tle syn­tac­tic sug­ar. Range based for loops could al­so have been im­ple­ment­ed us­ing on­ly lamb­da and a tem­plate func­tion. constexpr func­tions can of­ten be re­placed with tem­plate metapro­gram­ming, but us­ing constexpr func­tions is usu­al­ly eas­i­er on the com­pil­er and re­sults in short­er com­pile times.