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

165 views Asked by At

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?

2

There are 2 answers

2
Artyer On BEST ANSWER

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;
}
0
Larry On

While you already checked Artyer's solution and even provided your own (easier) wrapper for it (which I would have done too though with my own preferred interface below), the code below demonstrates another way. It's easier to understand than Artyer's solution IMHO (and also uses concepts), though kudos to him for his own. His is the more canonical way in C++ though I didn't review it in detail (but presumably you confirmed it works). However, the looping syntax he demonstrated is brutal IMHO but through no fault of his own. His technique is how it's generally done in C++ (or similar).

My own solution below is a similar (generic) function template called "ForEach()" which is a stripped down version from my "FunctionTraits" library here (so that anyone reading this doesn't have to download the library to see it). The version below targets C++20 or later only however and all comments have been removed (since they're pretty long and I didn't want to clutter things up with them here - grab "TypeTraits.h" in the above repository if you wish to see them). Variations of this technique that can handle earlier versions are also doable of course, and if you're able to use the above library itself (effectively single header only and pretty short for what it offers), it actually targets C++17 or later (though again, implementations targeting earlier versions are doable).

Note that while the library is fully documented at the above link (should you decide to use it instead of the stripped down version below), "ForEach()" is only (fully) documented in the "TypeTraits.h" file that comprises the library. See it in that file for details. It's not documented at the link itself since the library's focus is specifically on the "FunctionTraits" template (see link), and "ForEach()" is just an internal support function for that. Only "FunctionTraits" is therefore documented at the link but none of its support templates (like "ForEach()"). The support templates are still available for public use for anyone who wishes to use them though, but only documented in "TypeTraits.h" itself (not the above link).

Lastly, note that in your own wrapper you posted here, you replaced the call to "function_of" in your original post with "(j > 2)" so I did the same below, and unless there's a mistake in your wrapper, it only iterates "k - 1" times in my (quick) test, not "k" times as per your original post (assuming I got all this correct). The code below continues to iterate "k" times so see comment just before "ForEach<k>" below if you need to change it to "k - 1".

Finally, note that I replaced "int" with "std::size_t" everywhere since it's normally more appropriate in the context of "constexpr" loops (and "ForEach()" itself always assumes it). In fact, I'm not sure if you need to be passing the type in your own templates but you know your own requirements of course (and I left the "int" in place in your own templates, but "std::size_t" is likely what you want).

Click here to run it:

#include <cstddef>
#include <type_traits>
#include <utility>
#include <iostream>

// Primary template
template <typename T, typename = void>
struct IsForEachFunctor : std::false_type
{
};

// Specialization of above template
template <typename T>
struct IsForEachFunctor<T,
                        std::enable_if_t<std::is_same_v<decltype(std::declval<T>().template operator()<std::size_t(0)>()), bool>>
                       > : std::true_type
{
};

// Helper variable template for "IsForEachFunctor" just above
template <typename T>
inline constexpr bool IsForEachFunctor_v = IsForEachFunctor<T>::value;

// Concept for "IsForEachFunctor_v" just above
template <typename T>
concept ForEachFunctor_c = IsForEachFunctor_v<T>;

// ForEach()
template <std::size_t N, ForEachFunctor_c ForEachFunctorT>
inline constexpr bool ForEach(ForEachFunctorT&& functor)
{
    const auto process = [&functor]<std::size_t I>
                         (const auto &process)
                         {
                             if constexpr (I < N)
                             {
                                 if (std::forward<ForEachFunctorT>(functor).template operator()<I>())
                                 {
                                     return process.template operator()<I + 1>(process);
                                 }
                                 else
                                 {
                                     return false;
                                 }
                             }
                             else
                             {
                                 return true;
                             }
                         };

    return process.template operator()<0>(process);
}

// Code you posted here: https://godbolt.org/z/Y7neeGfYe
template <typename T, int i, int j, int k>
T method_one(T x) { int unused[j - 2]; static_cast<void>(unused); return x; }

// Code you posted here: https://godbolt.org/z/Y7neeGfYe
template <typename T, int i, int j, int k>
T method_two(T x) { return x + 1; }

// Your original posted function (modified to call "ForEach" above)
template <typename T, std::size_t k>
T function(T x)
{
    //////////////////////////////////////////////////////////
    // Lambda template that will be invoked "k" times in call
    // to "ForEach()" just below, where template arg "I" in
    // the lambda is passed on each invocation (in range 0 to
    // k EXclusive). Note that lambda templates are supported
    // in C++20 and later only.
    //////////////////////////////////////////////////////////
    const auto processI = [&]<std::size_t I>()
                             {
                                  // "I" is zero-based, your own "i" isn't (so adding 1 to conform to what you want)
                                  constexpr std::size_t i = I + 1;

                                  //////////////////////////////////////////////////////////
                                  // Lambda template that will be invoked "i" times in call
                                  // to "ForEach()" just below, where template arg "J" in
                                  // the lambda is passed on each invocation (in range 0
                                  // to i Exclusive). Note that lambda templates are
                                  // supported in C++20 and later only.
                                  //////////////////////////////////////////////////////////
                                  const auto processJ = [&]<std::size_t J>()
                                                        {
                                                             constexpr std::size_t j = I - J;
                                                             constexpr bool method_one_ok = (j > 2);
                                                             if constexpr (method_one_ok)
                                                             {
                                                                 x = method_one<T, i, j, k>(x);
                                                             }
                                                             else
                                                             {
                                                                 x = method_two<T, i, j, k>(x);
                                                             }

                                                            //////////////////////////////////////////////
                                                            // Return true to continue iterating (false
                                                            // would stop iterating, equivalent to a
                                                            // "break" statement in a regular "for" loop)
                                                            //////////////////////////////////////////////    
                                                            return true;;
                                                        };

                                  ///////////////////////////////////////
                                  // Iterate "i" times, invoking lambda
                                  // "processJ" above on each iteration.
                                  // Returns the return value of the
                                  // lambda which is always true just
                                  // above (so iteration continues until
                                  // the end - false return would stop
                                  // iterating, equivalent to a break"
                                  // statement in a regular "for" loop)
                                  ///////////////////////////////////////
                                  return ForEach<i>(processJ);
                              };

    ///////////////////////////////////////
    // Iterate "k" times, invoking lambda
    // "processI" above on each iteration.
    // Do you actually need this "k - 1"
    // times though (your own wrapper for
    // Artyer's code changed it to that
    // from your original post). If so
    // then change "k" to "k - 1" here
    // though if "k" is zero then you'll
    // have an issue of course (not sure
    // what your actual requirements are).
    ///////////////////////////////////////
    ForEach<k>(processI);

    return x;
}

int main()
{
    int x = 3;
    std::cout << function<std::size_t, 4>(x);

    return 0;
}