Search code examples
c++templateslambdaswitch-statementvariadic-templates

Switch with number of cases being dependent on template parameter


I need an efficient way to turn a runtime-dynamic int in a range from 0 to N-1 into a template argument.

I.e., I want to create - logically speaking - a switch that switches a number between 0 and N-1. N is a template parameter, so the number of cases is fixed at compile time, but it is dependent on the template argument, so I cannot just use a normal switch.

The goal is to call a template lambda for each case where the switched-upon number is passed to the lambda as a template argument. I.e., I want to go from a dynamic number to a static template argument. So in pseudo-code, I want to reach something like this:

template<size_t N, typename Lambda>
auto switchOverN(int n, Lambda lambda) {
   assert(n < N && n >= 0);
   switch (n) {
      case 0: return lambda<0>();
      case 1: return lambda<1>();
      ...
      case N-1: return lambda<n-1>();
   }
}

The solution should only use templates, no macros. Of course I cannot use a normal switch due to the varying number of cases.

How can I achieve this? Here is what I have so far.

I use a combination of std::variant, std::make_index_sequence, & std::integral_constant to give me a variant of all integral constants between 0 and N-1:

template<size_t I>
struct IndexVariantHelper {
   template<std::size_t... I>
   std::variant<std::integral_constant<int, I>...> toVariant(std::index_sequence<I...>) { return {}; }

   typename type = decltype(toVariant(std::make_index_sequence<I>()));
}

So now I can use IndexVariantHelper<N>::type to get a variant of index sequences and I can use std::visit to perform the switch. So I am almost done...:

template<size_t N, typename Lambda>
auto switchOverN(int n, Lambda lambda) {
   typename IndexVariantHelper<N>::type switcher = ... // Here's the missing part
   return std::visit([&]<typename Idx>(const Idx&) {
      constexpr size_t i = Idx::value;
      return lambda<i>();
   }, switcher);

So the switching can now be done, nice! And the good news is that the resulting code is blazingly efficient. The compiler can look through all the templates and indeed generate very efficient code for the switch. It can even constant fold it - depending on the lambda of course. Here is an example: https://godbolt.org/z/v31sdKWv1

My only remaining problem is how to get the dynamic input n into the variant. I am running out of ideas here.


Solution

  • Array of functor does that job

    template <std::size_t N>
    auto toIntegralConstantVariant(std::size_t n)
    {
        return [&]<std::size_t... Is>(std::index_sequence<Is...>){
            return std::array{
                +[]() -> std::variant<std::integral_constant<size_t, Is>...>{
                    return std::integral_constant<size_t, Is>{};
                }...
            }[n]();
        }(std::make_index_sequence<N>{});
    }
    

    Demo