Search code examples
c++template-meta-programming

C++ template metaprogramming nested switch statement


Let's assume I have an union type (variant type) like this one (simplified just for demonstration purposes)

enum class Type : uint32_t
{
  Unknown = 0,
  Int64 = 1,
  UInt64 = 2,
  Double = 3
};

struct Value
{
  Type dtype;
  union
  {
    int64_t i;
    uint64_t u;
    double d;
  };
}

And I intend to dispatch this value to different functions like this (actually function objects, but simplifying again...),

template<typename... T> auto eval(const T&... t)
{
   // ...
}

which takes a parameters pack, or simpler ones like:

template<typename T1, typename T2> auto sum(const T1& a, const T2& b)
{
   return a+b;
}

In general I need a switch statement with the following structure:

  switch (o1.dtype)
  {
    case Type::Int64:
      switch (o2.dtype)
      {
        case Type::Int64:
          F(o1.i, o2.i);
        case Type::Double:
          F(o2.i, o2.d);
        //...
      }
      break;
    case Type::Double
      //...
      

for the case of invoking a functor with 2 parameters. But it gets much worse for the case of 3 parameters...

Is there any way to generalize it via metaprogramming? Ideally I would have only one switch statement, and all the nested ones generated.


Solution

  • You could make a variadic templated function that does the switch on the first argument and calls itself with the rest and a lambda that saves the first argument's type:

    template<typename F, typename... T>
    // decltype(auto) is used to deduce the return type from F, throughout
    decltype(auto) apply_func(F f, const Value& v, T&&... vs) { // can't say "any number of arguments, all Values", so say "any arguments of any type" and just convert them to Value later
        switch (v.dtype) {
        case Type::Int64: // bind first argument of f to chosen variant of v, then apply new function (with one less argument) to remaining Values
            return apply_func(
                [&](const auto&... args) -> decltype(auto) {
                    return f(v.i, args...);
                }, std::forward<T>(vs)...
            );
        case Type::UInt64:
            return apply_func(
                [&](const auto&... args) -> decltype(auto) {
                    return f(v.u, args...);
                }, std::forward<T>(vs)...
            );
        case Type::Double:
            return apply_func(
                [&](const auto&... args) -> decltype(auto) {
                    return f(v.d, args...);
                }, std::forward<T>(vs)...
            );
        default: throw std::invalid_argument("Unknown dtype");
        }
    }
    
    // final overload, when all arguments have been switched on and the chosen variants have been saved into f
    template<typename F>
    decltype(auto) apply_func(F f) {
        return f();
    }
    

    Godbolt demo