Avoiding code duplication for runtime-to-compile-time numeric parameter translation
Asked Answered
L

2

5

Suppose we have function such as

template <typename T, unsigned N> void foo();

and for simplicity assume that we know that only (constant) values N_1, N_2 ... N_k are valid for N.

Now, suppose I want to make that compile-time parameter a run-time one, using foo() as a black-box, i.e. implement:

template <typename T> void foo(unsigned n);

by making foo<,>() calls. How should I go about doing that? Obviously, I can write:

template <typename T> void foo(unsigned n) {
    switch(n) {
    case N_1 :  foo<T, N_1>(); break;
    case N_2 :  foo<T, N_2>(); break;
    // etc. etc.
    case N_k :  foo<T, N_k>(); break;
    }
 }

... but this makes me feel all dirty. I could use a MAP() meta-macro to generate these k lines, I suppose; but can I do anything better and less-macroish to achieve the same? Is it possible to write something like the above that's general, and works for every variadic template and a fixed sequence of constant values?

Notes:

  • C++11/14/17-specific suggestions are obviously welcome.
  • The N's are not necessarily contiguous, nor small, nor sorted. e.g. suppose N_2 = 123456789 and N_5 = 1.
Lexicon answered 27/7, 2016 at 14:50 Comment(0)
R
6

You could make a function pointer table:

using F = void(*)();

template <class T, class >
struct Table;

template <class T, size_t... Is>
struct Table<T, std::index_sequence<Is...> > {
    static constexpr F fns[] = {
        foo<T, Is>...
    };
};

template <class T, size_t... Is>
constexpr F Table<T, std::index_sequence<Is...> >::fns[sizeof...(Is)];

And then just invoke the one you want:

template <class T, size_t N>
struct MakeTable : Table<T, std::make_index_sequence<N>> { };

template <typename T>
void foo(unsigned n) {
    MakeTable<T, MaxN>::fns[n]();
}

If the N_ks aren't contiguous, then we can use a lambda for inline parameter unpacking:

 template <class T>
 void foo(unsigned n) {
     using seq = std::index_sequence<N_1, N_2, ..., N_k>;
     indexer(seq)([n](auto i){
         if (n == i) {
             f<T, i>();
         }
     });
}

If the above is too slow, then I guess just manually build a std::unordered_map<unsigned, void(*)()> or something.

Rive answered 27/7, 2016 at 15:22 Comment(3)
@Lexicon Oh are the N_ks not contiguous?Rive
Nope, I never implied they were.Lexicon
Actually your indexer should probably be faster than an unordered map unless k is large (assuming the compiler is smart enough). +1.Lexicon
H
3

In these kind of situations I like to build a static table of function pointers, with a dynamic parameter deciding which one to dispatch to. Below is an implementation that achieves this, in the function foo_dynamic. To this function, you specify the maximum value of N you'd like to support, and it builds a static table of function pointers using some recursive templates. You then dereference into this table with your dynamic parameter.

using ftype = void (*)();

template <typename T, unsigned N> void foo()
{
    std::cout << N << std::endl;
}

template <typename T, unsigned max>
struct TablePopulator
{
    static void populateFTable(ftype* table)
    {
        table[max] = foo<T,max>;
        TablePopulator<T,max-1>::populateFTable(table);
    }
};

template <typename T>
struct TablePopulator<T, 0>
{
    static void populateFTable(ftype* table)
    {
        table[0] = foo<T,0>;
    }
};

template<typename T, unsigned max_N>
std::array<ftype, max_N>& initTable()
{
    static std::array<ftype, max_N> table;
    TablePopulator<T, max_N-1>::populateFTable(table.data());
    return table;
}

template<typename T, unsigned max_N>
void foo_dynamic(unsigned actualN)
{
    static auto ftable = initTable<T, max_N>();
    if(actualN >= max_N)
        throw std::runtime_error("Max param exceeded");

    ftable[actualN]();
}


int main()
{
    foo_dynamic<int, 10>(1);
    foo_dynamic<int, 10>(5);

   return 0;
}

EDIT: Given the constraints in the question edit, here's an approach where valid indices are specified manually, which uses an unordered_map instead of an array:

using ftype = void (*)();

template <typename T, unsigned N> void foo()
{
    std::cout << N << std::endl;
}

template<typename T, size_t ... Indices>
void foo_dynamic_indices(size_t actual_index)
{
    static std::unordered_map<size_t, ftype> fmap = {{Indices, foo<T,Indices>}...};
    auto fIt = fmap.find(actual_index);
    if(fIt == fmap.end())
        throw std::runtime_error("Index not found");

    fIt->second();
}

int main()
{

    foo_dynamic_indices<int, 0, 3, 400, 1021, 10000000>(10000000);
    foo_dynamic_indices<int, 0, 3, 400, 1021, 10000000>(4); //Exception

   return 0;
}
Hydrostatics answered 27/7, 2016 at 15:23 Comment(3)
Suppose N_k = 10000000.Lexicon
Yeah, works for that too (with appropriate -ftemplate-depth=10000000 switch), although compiles very slowlyHydrostatics
There's an at() for unordered_map too, so you could simplify the call to just fmap.at(actual_index)() (throws out_of_range instead of runtime_error)Rive

© 2022 - 2024 — McMap. All rights reserved.