C++ runtime type switching (avoiding switch)

安稳与你 提交于 2019-11-30 07:15:05

问题


I've been into C++ for some years but I have not found yet the solution to a problem I constantly have. Know how to solve it would be awesome.

What I have at the moment is:

// Client code:
switch(currentEnumValue)
    {
    case MyEnum::kValue01:
      processData<MyEnum::kValue01>(data);
      break;
    case MyEnum::kValue02:
      processData<MyEnum::kValue02>(data);
      break;
    default:
      LOG("Invalid command");
      break;
    }

// Declarations

enum class MyEnum {kValue01, kValue02};
class MyClass
{
// code
template <MyEnum> void processData(char*); /* Implemented somewhere else */
}
  template <> void MyClass::processData<MyEnum::kValue01>(char* data); /* Implemented somewhere else */
  MyClass <> void MyClass::processData<MyEnum::kValue02>(char* data); /* Implemented somewhere else */

I would like to remove the switch because of many reasons. Instead of it I would need something like: processData<runtime-decltype(currentEnumValue)>(data);

I know about typeid and about not mixing compile time and runtime together... but despite this, I would like to find some solution anyway, preferably excluding macros.


回答1:


This class makes a jump table for a given Enum up to a certain count size based off constructing some template and invoking it with the supplied args. It assumes the enum values start at 0, and go to Count-1.

template<class Enum, Enum Count, template<Enum>class Z>
struct magic_switch {
  // return value of a call to magic_switch(Args...)
  template<class...Args>
  using R = std::result_of_t<Z<Enum(0)>(Args...)>;
  // A function pointer for a jump table:
  template<class...Args>
  using F = R<Args...>(*)(Args&&...);
  // Produces a single function pointer for index I and args Args...
  template<size_t I, class...Args>
  F<Args...> f() const {
    using ret = R<Args...>;
    return +[](Args&&...args)->ret{
      using Invoke=Z<Enum(I)>;
      return Invoke{}(std::forward<Args>(args)...);
    };
  }
  // builds a jump table:
  template<class...Args, size_t...Is>
  std::array<F<Args...>,size_t(Count)>
  table( std::index_sequence<Is...> ) const {
    return {{
      f<Is, Args...>()...
    }};
  }
  template<class...Args>
  R<Args...> operator()(Enum n, Args&&...args) {
    // a static jump table for this case of Args...:
    static auto jump=table<Args...>(std::make_index_sequence<size_t(Count)>{});
    // Look up the nth entry in the jump table, and invoke it:
    return jump[size_t(n)](std::forward<Args>(args)...);
  }
};

then if you have an enum:

enum class abc_enum { a, b, c, count };

and a function object template:

template<abc_enum e>
struct stuff {
  void operator()() const {
    std::cout << (int)e << '\n';
  }
};

you can dispatch:

magic_switch<abc_enum, abc_enum::count, stuff>{}(abc_enum::b);

in any case, within the template stuff, you get the enum value as a compile time constant. You call it with a run time constant.

Overhead should be similar to a switch statement, or a vtable call, depending on what the compiler does optimization wise.

live example.

Note that setting Enum to std::size_t is valid.

In C++11 you need make_index_sequence and index_sequence:

template<size_t...>
struct index_sequence {};
namespace details {
  template<size_t Count, size_t...szs>
  struct sequence_maker : sequence_maker<Count-1, Count-1, szs...> {};
  template<size_t...szs>
  struct sequence_maker<0,szs...> {
    using type = index_sequence<szs...>;
  };
}
template<size_t Count>
using make_index_sequence=typename details::sequence_maker<Count>::type;
template<class...Ts>
using index_sequence_for=make_index_sequence<sizeof...(Ts)>;

and this alias:

template<class Sig>
using result_of_t=typename std::result_of<Sig>::type;

then strip std:: off their use in the above code.

live example.




回答2:


Boost variant does something like what you are doing. It lets you replace switch statements with a template based contruct that can check that all cases are defined at compile-time, but then select one at run-time.

e.g.,

using namespace boost;
using Data = variant<int, double>;

struct ProcessDataFn: static_visitor<void>
{
    char* data;
    void operator()(int& i)
    {
        // do something with data
    }

    void operator()(double& d)
    {
        // do something else
    }
};

void processData(char* data, Data& dataOut)
{
    apply_visitor(ProcessDataFn{data}, dataOut);
}

void example(char * data)
{
    Data d = 0;
    processData(data, d); // calls first overload of operator()
    Data d = 0.0;
    processData(data, d); // calls second overload
}



回答3:


To expand on my comment, ideally we'd have compile-time reflection and be able to write a generic dispatch function. In its absence, one option is to unfortunately use macros to do that for you using the X Macro pattern:

#define LIST_OF_CASES   \
    X_ENUM(kValue0)     \
    X_ENUM(kValue1)     \
    X_ENUM(kValue2)


enum MyEnum
{
#   define X_ENUM(a) a,
    LIST_OF_CASES
#   undef X_ENUM
};

void dispatch(MyEnum val)
{
    switch (val)
    {
#       define X_ENUM(a) case a: processData<a>(); break;
        LIST_OF_CASES
#       undef X_ENUM
    default:
        // something's really wrong here - can't miss cases using this pattern
    }
}

One benefit of this approach is that it scales to large numbers of enumerations, it gets really hard to omit a case, and that you can attach extra information by using a multi-argument X_ENUM macro.

I know you said you'd like to avoid macros, but the alternative without virtual functions then is to have some sort of a static table of function pointers indexed by the enum, and that is just a virtual function in disguise (with admittedly lower overhead, but still suffering the cost of an indirect function call).



来源:https://stackoverflow.com/questions/30640853/c-runtime-type-switching-avoiding-switch

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!