C++ runtime type switching (avoiding switch)

后端 未结 3 1255
感情败类
感情败类 2020-12-09 20:47

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 mo

相关标签:
3条回答
  • 2020-12-09 21:33

    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.

    0 讨论(0)
  • 2020-12-09 21:38

    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).

    0 讨论(0)
  • 2020-12-09 21:48

    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
    }
    
    0 讨论(0)
提交回复
热议问题