c++templatesc++17template-meta-programmingindex-sequence

Concisely map runtime variables to template parameters, for _many_ functions


Problem

I have a templated function

template <int N>
void myfunc(int a) {

    // contents compile-time optimised using N
}

which I instantiate at compile-time to make unique definitions for differing N. At runtime, I need to invoke myfunc with the template parameter informed by a user-given variable.

A verbose method would be:

void callOptimisedMyFunc(int n, int a) {
    if (n == 0) myfunc<0>(a);
    if (n == 1) myfunc<1>(a);
    if (n == 2) myfunc<2>(a);
    // ...
    if (n == MAX) myfunc<MAX>(a);
}

which is painfully long in my case, whereby MAX can be as large as 64. To make matters worse, I need to create such wrappers for about ten distinct functions with unique signatures and return types! E.g.

template <int N>
long otherFunc(string x, double y) {
    
    // ...

} 

void callOptimisedOtherFunc(int n, string x, double y) {
    if (n == 0) return otherfunc<0>(x, y);
    if (n == 1) return otherfunc<1>(x, y);
    if (n == 2) return otherfunc<2>(x, y);
    // ...
    if (n == MAX) return otherfunc<MAX>(x, y);
}

I ergo wish to perform this mapping of run-time to compile-time parameter metaprogrammatically.

Unsatisfying solution

My current unsatisfyingly verbose solution is to use log-depth macros:

#define NORETURN

#define INNER1(usernum, compnum, func, args, retcmd) \
    if (usernum == compnum) retcmd func<compnum> args;

#define INNER4(usernum, compbase, func, args, retcmd) \
    INNER1( usernum, compbase+0, func, args, retcmd ); \
    INNER1( usernum, compbase+1, func, args, retcmd ); \
    INNER1( usernum, compbase+2, func, args, retcmd ); \
    INNER1( usernum, compbase+3, func, args, retcmd );

#define INNER16(usernum, compbase, func, args, retcmd) \
    INNER4( usernum, compbase+0,  func, args, retcmd ); \
    INNER4( usernum, compbase+4,  func, args, retcmd ); \
    INNER4( usernum, compbase+8,  func, args, retcmd ); \
    INNER4( usernum, compbase+12, func, args, retcmd );

#define INNER64(usernum, func, args, retcmd) \
    INNER16( usernum, 0,  func, args, retcmd ); \
    INNER16( usernum, 16, func, args, retcmd ); \
    INNER16( usernum, 32, func, args, retcmd ); \
    INNER16( usernum, 48, func, args, retcmd );

#define CALL_OPTIMISED_FUNC(n, func, ...) \
    INNER64( n, func, (__VA_ARGS__), NORETURN );

#define CALL_AND_RETURN_OPTIMISED_FUNC(n, func, ...) \
    INNER64( n, func, (__VA_ARGS__), return );

which permits me to run:

void callOptimisedMyFunc(int N, int a) {

    CALL_OPTIMISED_FUNC( N, myfunc, a );
}

long callOptimisedOtherFunc(int N, string x, double y) {

    CALL_AND_RETURN_OPTIMISED_FUNC( N, otherfunc, x, y );
}

This is still uncomfortably verbose, and does not use all the beautiful C++ metaprograming facilities available in C++17. It also seems a shame to needlessly evaluate 64 if statements at each invocation. A more satisfying solution would be to prepare a static array of function pointers, indexed by their template parameter N.

Partial solution

If I only had a single templated function to call (i.e. only myfunc), I could use index_sequence like so:

#include <array>
#include <utility>

template<size_t... N>
constexpr auto helper_myfunc(index_sequence<N...>) {

    return array <void (*)(int), sizeof...(N)> { myfunc<N> ... };
}

constexpr auto getArrayOfMyFuncRefs() {

    return helper_myfunc( make_index_sequence<MAX>() );
}

which would allow me to concisely write

void callOptimisedMyFunc(int n, int a) {

    static auto funcs = getArrayOfMyFuncRefs();
    funcs[n](a);
}

Alas, I would need to duplicate this definition for the other functions for which I wish to map run-time parameters to compile-time ones:

template<size_t... N>
constexpr auto helper_otherfunc(index_sequence<N...>) {

    return array <long (*)(string, double), sizeof...(N)> { otherfunc<N> ... };
}

constexpr auto getArrayOfOtherFuncRefs() {

    return helper_otherfunc( make_index_sequence<MAX>() );
}
long callOptimisedOtherFunc(int n, string x, double y) {

    static auto funcs = getArrayOfOtherFuncRefs();
    return funcs[n](x, y);
}

Sought solution

I seek to generalise my getArray*() functions above to a single function which accepts any singly-integer-templated function, even with differing signatures. That would allow me to concisely define:

void callOptimisedMyFunc(int n, int a) {

    static auto funcs = getArrayOfFuncRefs(myfunc);
    funcs[n](a);
}

void callOptimisedOtherFunc(int n, string x, double y) {

    static auto funcs = getArrayOfFuncRefs(otherfunc);
    return funcs[n](x, y);
}

I am struggling to do this because I have no idea how I can pass a reference like myfunc to a function; it lacks its template syntax (i.e. <0>). My closest efforts do not compile:

template<size_t... N, typename RetType, typename... Args>
constexpr auto helper(index_sequence<N...>, ReturnType (*funcname)(Args...)) {

    return array<RetType (*)(Args...), sizeof...(N)>{ funcname<N> ... };
}

template<typename Func>
constexpr auto getArrayOfFuncRefs(Func func) {

    return helper(make_index_sequence<MAX>(), func);
}

Is there a way to achieve what I want with C++ metaprogramming, or must I fall back onto my horrible usage of macros?


Solution

  • One way to transform runtime value to compile time value is to use std::variant, and then use std::visit for the dispatch:

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

    Mostly the same as your helper function, but to have

    std::variant<
        std::integral_constant<std::size_t, 0>,
        std::integral_constant<std::size_t, 1>,
        //..
        std::integral_constant<std::size_t, Max - 1>,
    >;
    

    Then the wrapping can be done as follow:

    template <std::size_t N>
    void myfunc(int a) {
        //...
    }
    
    void CallOptimizedMyFunc(std::size_t n, int a)
    {
        std::visit([&](auto N){ myfunc<N()>(a); },
            to_integral_constant_var<Max>(n));
    }
    

    Demo