c++templatesc++17

Creating Cartesian Product from Integer Range Template Argument


I am working with a third party library which provides a Numeric class that must be templated with precision and scale arguments. Unfortunately, within the context of the library I am creating, the precision and scale is not known until runtime, so I need to create a runtime mapping of those values to their respective templated functions.

The Numeric library supports up to 38 decimal digits, so in theory I need to create a mapping of the form:

mapping = {
  <38, 38>: []() { // do something with Numeric<38, 38> ... },
  <38, 37>: []() { // do something with Numeric<38, 37> ... },
  ...
  <37, 37>: []() { // do something with Numeric<38, 37> ... },
  <37, 36>: []() { // do something with Numeric<38, 37> ... },
  ...
}

For all precision and scale arguments in the range (0, 38], where scale <= precision

This is the code I've created, ignoring the implementation of the std::function map values and using a smaller parameter pack for now:

using funcMapType = std::map<std::pair<int, int>, std::function<void(void)>>;

template <int P, int S, int... Rest> struct NumericCreatorInserter {
  static void insert(funcMapType &func_map) {
    NumericCreatorInserter<P, P>::insert(func_map);
    NumericCreatorInserter<P, S>::insert(func_map);
    NumericCreatorInserter<P, Rest...>::insert(func_map);
    NumericCreatorInserter<S, Rest...>::insert(func_map);
  }
};

template <int Precision, int Scale>
struct NumericCreatorInserter<Precision, Scale> {
  static void insert(funcMapType &func_map) {
    std::cout << "Precision is: " << Precision << " and scale is: " << Scale
              << std::endl;
    func_map.emplace(std::make_pair(Precision, Scale), [](void) { return; });
  }
};

static funcMapType initializeNumericCreatorMap() {
  funcMapType numeric_creators;
  NumericCreatorInserter<3, 2, 1, 0>::insert(numeric_creators);
  return numeric_creators;
};

Debugging this at runtime, I get the following:

Precision is: 3 and scale is: 3
Precision is: 3 and scale is: 2
Precision is: 3 and scale is: 3
Precision is: 3 and scale is: 1
Precision is: 3 and scale is: 0
Precision is: 1 and scale is: 0
Precision is: 2 and scale is: 2
Precision is: 2 and scale is: 1
Precision is: 2 and scale is: 0
Precision is: 1 and scale is: 0

I am expecting to see pairs of (3, 3), (3, 2), (3, 1), (3, 0), (2, 2), (2, 1), etc...

I think I understand the issue with my code - the sequence starting with (3, 3), (3, 1), (3, 0), and (1, 0) happens when P = 3, S = 1, and Rest...=0, which occurs during the first recursive invocation of the function

With that said, I am struggling with how to express what I am trying to achieve in C++. Is there another template specialization I should be introducing to get this to work?

Ideally I am looking for a solution that works with C++17


Solution

  • A way to transform runtime value into compile time value is to use std::variant:

    template <std::size_t N>
    constexpr auto to_integral_variant(std::size_t n)
    {
        return [&]<std::size_t... Is>(std::index_sequence<Is...>) {
            using ResType = std::variant<std::integral_constant<std::size_t, Is>...>;
           ResType all[] = {ResType{std::integral_constant<std::size_t, Is>{}}...};
           return all[n];
        }(std::make_index_sequence<N>());
    }
    

    then, dispatch with std::visit:

    auto foo(std::size_t prec, std::size_t scale)
    {
        return std::visit([](auto P, auto S) {
            return func<P(), S()>();
        }, to_integral_variant<MAX>(prec), to_integral_variant<MAX>(scale));
    }
    

    Demo

    To "ensure" that P() <= S() in your code, you might use if constexpr:

    return std::visit(
        [](auto P, auto S) {
            if constexpr (S() <= P()) {
                return func<P(), S()>();
            } else {
                throw "unreachable";
            }
        }, to_integral_variant<MAX>(prec), to_integral_variant<MAX>(scale));
    

    or even group the pair

    template <std::size_t N>
    struct Helper
    {
        template <std::size_t P>
        struct Inner {
            using type = decltype(
                []<std::size_t... Is>(std::index_sequence<Is...>){
                    return std::tuple<
                        std::integral_constant<std::pair<std::size_t, std::size_t>,
                                               std::pair(P, Is)>...>();
                }(std::make_index_sequence<P + 1>())
            );
        };
        using type = typename tuple_to_variant<
            decltype(
                []<std::size_t... Is>(std::index_sequence<Is...>){
                    return std::tuple_cat(typename Inner<Is>::type{}...);
                }(std::make_index_sequence<N>())
            )
        >::type;
    };
    
    template <std::size_t N>
    constexpr auto to_integral_pair_variant(std::size_t prec, std::size_t scale)
    {
        return std::visit(
            [](auto P, auto S) -> Helper<N>::type
            {
                if constexpr (S() <= P()) {
                    return std::integral_constant<std::pair<std::size_t, std::size_t>,
                                               std::pair(P(), S())>{};
                } else {
                    throw "unreachable";
                }
            }, to_integral_variant<N>(prec), to_integral_variant<N>(scale)
        );
    }
    

    and then

    auto foo(std::size_t prec, std::size_t scale)
    {
        return std::visit(
            [](auto P) {
                return func<P().first, P().second>();
            }, to_integral_pair_variant<MAX>(prec, scale));
    }
    

    Demo