c++templatesindex-sequence

How to pass template parameters from indices generated in a loop?


Suppose I want to call some templates over and over in a loop, with the loop indexes in the template arguments. Like this (except not illegal):

template <typename T, int k>
T function(T x) {
    for (int i = 1; i <= k; ++i) {
        for (int j = i - 1; j >= 0; --j) {
            constexpr bool method_one_ok = function_of(i, j);
            if (method_one_ok) {
                x = method_one<T, i, j, k>(x);
            } else {
                x = method_two<T, i, j, k>(x);
            }
        }
    }
    return x;
}

I know how to hobble through it with recursive templates and specializations, but that's a horror I figured out around about C++11 or earlier.

Is there a cleaner way to do this?


Solution

  • Here's a handy function:

    template<typename T, T... I, typename F>
    constexpr void idx_for_each(F&& f, std::integer_sequence<T, I...>) {
        (static_cast<void>(f(std::integral_constant<T, I>{})), ...);
    }
    

    It will call a function with every integer in an integer_sequence wrapped in an integral_constant. For example:

    idx_for_each(f, std::make_index_sequence<3>{})
    // Calls
    f(std::integral_constant<std::size_t, 0>{});
    f(std::integral_constant<std::size_t, 1>{});
    f(std::integral_constant<std::size_t, 2>{});
    

    Then with some clever transformations from 0 -> X to your desired ranges 1 -> k+1 and i-1 -> -1, you can write:

    template <typename T, int k>
    T function(T x) {
        idx_for_each([&](auto ii) {
            constexpr int i = ii + 1; 
            idx_for_each([&](auto jj) {
                constexpr int j = i - jj - 1;
                constexpr bool method_one_ok = function_of(i, j);
                if (method_one_ok) {
                    x = method_one<T, i, j, k>(x);
                } else {
                    x = method_two<T, i, j, k>(x);
                }
            }, std::make_integer_sequence<int, i>{});
        }, std::make_integer_sequence<int, k>{});
        return x;
    }
    

    This can be made to work in C++14 by replacing the fold expression:

    template<typename T, T... I, typename F>
    constexpr void idx_for_each(F f, std::integer_sequence<T, I...>) {
        // (static_cast<void>(f(std::integral_constant<T, I>{})), ...);
        using consume = int[];
        static_cast<void>(consume{ (static_cast<void>(f(std::integral_constant<T, I>{})), 0)... });
    }
    

    And can be made C++11 by implementing std::integer_sequence/std::make_integer_sequence for C++11.

    You can make this easier for your specific case by having a helper range<start, stop> that is an integer sequence from start to stop directly so you don't have to manipulate the function argument.


    In general, "template loops" are implemented by creating a new pack with std::index_sequence and applying it to a function, and folding over that pack.

    The C++20 solution is to do this totally in line with a lambda with a template parameter list:

    template <typename T, int k>
    T function(T x) {
        [&]<int... ii>(std::integer_sequence<int, ii...>) {
            ([&]{
                constexpr int i = ii + 1;
                [&]<int... jj>(std::integer_sequence<int, jj...>) {
                    ([&]{
                        constexpr int j = i - jj - 1;
                        constexpr bool method_one_ok = function_of(i, j);
                        if constexpr (method_one_ok) {
                            x = method_one<T, i, j, k>(x);
                        } else {
                            x = method_two<T, i, j, k>(x);
                        }
                    }, ...);
                }(std::make_integer_sequence<int, i>{});
            }(), ...);
        }(std::make_integer_sequence<int, k>{});
        return x;
    }