Search code examples
c++c++17variadic-templatestemplate-meta-programmingfold-expression

Improving fold function


I have implemented a simple fold function in C++ that accepts a lambda, and can fold multiple vectors at the same time at compile time. I am wondering if it could be simplified in some manner (I have provided both a recursive version and an iteratively recursive version - I am unsure which should have better performance): https://godbolt.org/z/39pW81

Performance optimizations are also welcome - in that regard is any of the two approaches faster?

template<int I, typename type_identity, typename type_head, int N, typename ...type_tail, int ...N_tail,  typename Function>
auto foldHelperR(Function&& func, const type_identity& id, const tvecn<type_head, N>& head, const tvecn<type_tail, N_tail>&... tail)
{
    if constexpr (I>0)
    {
        return func(foldHelperR<I-1>(std::forward<Function>(func), id, head, tail...), head[I], tail[I]...);
    }
    else
    {
        return func(id, head[0], tail[0]...);
    }
}

template<int I, typename type_identity, typename type_head, int N, typename ...type_tail, int ...N_tail,  typename Function>
auto foldHelperI(Function&& func, const type_identity id, const tvecn<type_head, N>& head, const tvecn<type_tail, N_tail>&... tail)
{
    if constexpr (I<N-1)
    {
        return foldHelperI<I+1>(std::forward<Function>(func), func(id, head[I], tail[I]...), head, tail...);
    }
    else
    {
        return func(id, head[N-1], tail[N-1]...);
    }
}

template<typename type_identity, typename type_head, int N_head, typename ...type_tail, int ...N_tail, typename Function = void (const type_identity&, const type_head&, const type_tail&...)>
constexpr auto fold(Function&& func, const type_identity& id, const tvecn<type_head, N_head>& head, const tvecn<type_tail, N_tail>&... tail)
{
    static_assert(std::is_invocable_v<Function, const type_identity&, const type_head&, const type_tail &...>,
     "The function cannot be invoked with these zip arguments (possibly wrong argument count).");
    static_assert(all_equal_v<N_head, N_tail...>, "Vector sizes must match.");

    //return foldHelperR<N_head-1>(std::forward<Function>(func), id, head, tail...);
    return foldHelperI<0>(std::forward<Function>(func), id, head, tail...);
}

int main()
{
    tvecn<int,3> a(1,2,3);
    return fold([](auto x, auto y, auto z) {return x+y+z;}, 0, a, a);
}

Solution

  • and can fold multiple vectors at the same time at compile time

    Not exactly: if you want to operate compile-time

    (1) you have to define constexpr the tvecn constructor and

    (2) you have to define constexpr the foldhelper function and

    (3) you have to declare constexpr a

     // VVVVVVVVV
        constexpr tvecn<int,3> a(1,2,3);
    

    (4) you have to place the result of fold in a constexpr variable (or, more generally speaking, in a place where the value is required compile time, as the size field of a C-style array, or a template value parameter, or a static_assert() test)

    constexpr auto f = fold([](auto x, auto y, auto z) {return x+y+z;},
                            0, a, a);
    

    I am wondering if it could be simplified in some manner

    Sure.

    First of all: if you can, avoid to reinventing the weel: your tvecn is a simplified version of std::array.

    Suggestion: use std::array (if you can obviously)

    Second: you tagged C++17 so you can use folding

    Suggestion: use it also for all_equal

    template <auto V0, auto ... Vs>
    struct all_equal : public std::bool_constant<((V0 == Vs) && ...)>
     { };
    
    template<auto ...N_pack>
    constexpr bool all_equal_v = all_equal<N_pack...>::value;
    

    More in general: when you have to define a custom type traits that has to provide a number, inherit (if possible) from std::integral_constant (or std::bool_constant, or std::true_type, or std::false_type: all std::integral_constant specializations). So you automatically inherit all std::integral_constant facilities.

    Third: almost all C++ standard uses std::size_t, not int, for sizes.

    Suggestion: when you have to do with sizes, use std::size_t, not int. This way you can avoid a lot of annoying troubles.

    Fourth: from main() you should return only EXIT_SUCCESS (usually zero) or EXIT_FAILURE (usually 1)

    Suggestion: avoid things as

    return fold([](auto x, auto y, auto z) {return x+y+z;}, 0, a, a);
    

    Fifth: never underestimate the power of the comma operator.

    Suggestion: avoid recursion at all and use template folding also for the helper function; by example

    template <std::size_t ... Is, typename F, typename T, typename ... As>
    constexpr auto foldHelperF (std::index_sequence<Is...>,
                                F const & f, T id, As const & ... arrs)
     { return ( ..., (id = [&](auto i){ return f(id, arrs[i]...); }(Is))); }
    

    that you can call as follows from fold()

    return foldHelperF(std::make_index_sequence<N_head>{}, 
                       std::forward<Function>(func),
                       id, head, tail...);
    

    The following is a full compiling, and simplified, example

    #include <array>
    #include <utility>
    #include <iostream>
    #include <type_traits>
    
    template <auto V0, auto ... Vs>
    struct all_equal : public std::bool_constant<((V0 == Vs) && ...)>
     { };
    
    template<auto ...N_pack>
    constexpr bool all_equal_v = all_equal<N_pack...>::value;
    
    
    template <std::size_t ... Is, typename F, typename T, typename ... As>
    constexpr auto foldHelperF (std::index_sequence<Is...>,
                                F const & f, T id, As const & ... arrs)
     { return ( ..., (id = [&](auto i){ return f(id, arrs[i]...); }(Is))); }
    
    
    template <typename type_identity, typename type_head, std::size_t N_head,
              typename ...type_tail, std::size_t ...N_tail,
              typename Function = void (type_identity const &,
                                        type_head const &,
                                        type_tail const & ...)>
    constexpr auto fold (Function && func, type_identity const & id,
                         std::array<type_head, N_head> const & head,
                         std::array<type_tail, N_tail> const & ... tail)
     {
       static_assert( std::is_invocable_v<Function, const type_identity&,
                      const type_head&, const type_tail &...>,
                      "The function cannot be invoked with these zip arguments"
                      " (possibly wrong argument count).");
    
       static_assert( all_equal_v<N_head, N_tail...>,
                     "Vector sizes must match.");
    
       return foldHelperF(std::make_index_sequence<N_head>{}, 
                          std::forward<Function>(func),
                          id, head, tail...);
    }
    
    int main()
     {
       constexpr std::array<int, 3u> b{2, 5, 7};
    
       constexpr auto f = fold([](auto x, auto y, auto z) {return x+y+z;},
                               0, b, b);
    
       std::cout << f << std::endl;
     }