Search code examples
c++templates

is there easier way to select appropriate template specialization at runtime in c++ without exhaustive coverage?


Say at runtime, a variable i can have a value from 1 to n. For each case, I need to call corresponding specialization of foo():

template<typename X>
void foo(X x) {
  ...
}

where X is to be one of X1, X2, ..., Xn depending on i.

Below is how I do it manually:

switch (i) {
  case 1: foo(X1()); break;
  case 2: foo(X2()); break;
  ...
  case n: foo(Xn()); break;
}

Is there a better way to do this? Even worse, if bar() takes two template parameters, then I'd have to do:

template<typename X, typename Y>
void bar(X x, Y y) {
  ...
}

int main() {
   ...
   if (i == 1 && j == 1) bar(X1(), Y1());
   else if (i == 1 && j == 2) bar(X1(), Y2());
   ...
   else if (i == 2 && j == 1) bar(X2(), Y1());
   ...
   else /* (i == n && j == m) */ bar(Xn(), Ym());
}

Wondering if there is a better / more clever way to do this.


Solution

  • As long as there is only one parameter, your manual approach is probably best. It's readable, and at some point you need to perform exactly that logic. That is, you need some sort of lookup to switch between function calls at runtime.

    I'll also echo some advice from the comments that you should make sure this is a real issue, not just an expected issue. Historically, many hours (years?) have been spent optimizing the wrong parts of various projects, resulting in no real benefit (but with a real maintenance cost since the code became more complex).


    If there are multiple parameters that can vary in type, then the manual approach does get bogged down. You still need a lookup of some sort to pick the function call at runtime, but perhaps you can hand off many of the details to the compiler. A little template magic could generate a lookup table for you. The magic is overkill for a single parameter, but it comes in handy when the number of parameters increases.

    I am going to take the question at face value in that the parameters are to be default constructed. However, I will change one detail in the numbering. Numbering in C++ is typically 0-based, not 1-based, so I'm going to assume there is an X0 and no Xn. This will make an array implementation smoother. The idea is to create an multi-dimensional array of function pointers that will serve as your lookup table. Use your i and j as indices to get the desired function. (You wanted raw efficiency, right? So I've used function pointers instead of std::function.)

    The template magic is a bit esoteric. There's a recursive class template definition, where each instantiation is responsible for defining a function template named make() that will return a possibly multi-dimensional array of function pointers (except for the base case, which will return a single function pointer; each recursion adds a dimension to the array). It is also responsible for defining a type alias, MakeType, for the type of the array being returned by make(). (While it is possible to get by without the alias, the code is easier to grasp with it.) At each stage of the recursion, one parameter is removed from the class template and one parameter is added to the make() template. The removed parameter is a tuple of the choices for the type of one parameter; the added parameter is one type from this tuple.

    Yes, this is designed to handle an arbitrary number of parameters, not just two. I'm going with the rule of thumb that sometimes a generic approach leads to cleaner code than focusing on a specific use case.

    #include <array>
    #include <tuple>
    #include <utility>
    
    template <typename ToInvoke, typename... Params>
    struct Invoker; // No definition yet.
    
    // Base case:
    template <typename ToInvoke>
    struct Invoker<ToInvoke> {
        // This is the effective signature of the functions:
        using MakeType = void (*)();
        
        template <typename... Values>
        static constexpr MakeType make() {
            // Returning a function pointer; the function is not invoked here.
            return ToInvoke::template invoke<Values...>;
        }
    };
    
    // Recursive case:
    template<typename ToInvoke, typename Param0, typename... Params>
    struct Invoker<ToInvoke, Param0, Params...> {
        // For readability:
    
        static constexpr auto param_size = std::tuple_size_v<Param0>; // Number of alternatives for this parameter
        template <std::size_t Index>
        using ParamType = std::tuple_element_t<Index, Param0>;        // Type of each alternative
        using Recurse = Invoker<ToInvoke, Params...>;                 // The recursive case (remove the first parameter)
    
        // Needed for recursion:
    
        // We will make an array. The length is the number of alternatives.
        // The elements are created by the recursive case.
        using MakeType = std::array<typename Recurse::MakeType, param_size>;
    
        // Make the array. Each alternative for the first parameter is appended
        // to `Values...` then given to the recursive case to get elements
        // for this array.
        template <typename... Values>
        static constexpr MakeType make() {
            // Immediately invoked lambda (to extract the indices):
            return []<std::size_t... Index>(std::index_sequence<Index...>) {
                return MakeType{ Recurse::template make<Values..., ParamType<Index>>()... };
            }(std::make_index_sequence<param_size>{});
        }
    };
    

    See? That's a lot of overhead. For the one-parameter case, the manual approach is cleaner.

    Okay, so once you have this template defined, how is it used? Well, one pesky detail is that a function template cannot be a template template parameter, so we need a wrapper for your function template. As a bonus, this wrapper will default-construct the parameters, which conveniently gives a uniform signature when invoking the wrapper. For this demonstration, I've expanded your function to three parameters and renamed it baz.

    struct baz_caller {
        template<typename X, typename Y, typename Z>
        static void invoke() { baz(X{}, Y{}, Z{}); }
    };
    

    So now, you just need to define some tuples with the desired types and call the appropriate make(). When you want to invoke a function, the indices to the types become indices to the array.

    // Define the valid parameter types.
    using Xs = std::tuple<X0, X1, X2, X3>;
    using Ys = std::tuple<Y0, Y1, Y2>;
    using Zs = std::tuple<Z0, Z1>;
    
    // Make a multi-dimensional array of function pointers.
    constexpr auto bazzer = Invoker<baz_caller, Xs, Ys, Zs>::make<>();
    
    int main () {
        unsigned i = 1, j = 1, k = 0;
        bazzer[i][j][k]();       // X1, Y1, Z0
        bazzer[--i][++j][++k](); // X0, Y2, Z1
    }
    

    One thing you lose with this approach is the ability to easily detect out-of-bounds indices (with a default on your switch). You might want to add some range checking around the call site, or you could change the sub-scripting from operator[] to at().