9

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
user3770392
  • 453
  • 5
  • 12
  • 2
    What about switching to dynamic polymorphism? Use an abstract interface (class with a pure virtual `processData()` function) and various implementations. – πάντα ῥεῖ Jun 04 '15 at 10:07
  • I am looking to avoid dynamic polymorphism as well, I forgot to write it. Thanks for the tip anyway (just needing the maximum possible performance) – user3770392 Jun 04 '15 at 10:11
  • 2
    These are not rhetorical questions, they determine which of several solutions I can think of is best. 1) Why do you need the enum as a compile time constant? 2) What features, *exactly*, of the switch are problematic? "several issues" is maddeningly vague. Please distinguish *must* from *like*. 3) Are the enum values zero-based, and contiguous? 4) What, exactly, about your proposed solution is "needed"? Please avoid over-specifying: the answer with too many "must" requirements may be "no, you cannot". – Yakk - Adam Nevraumont Jun 04 '15 at 10:18
  • Nice points, ok I'll try to answer: 1) I haven't understood the question perhaps, enums are not compile time constants? anyway, in this specific case I use these enums for many things and I need to know their value because I have to pass this it across the network inside a packet 2) problematic because I would like to avoid writing that switch code, especially if required several times (I can think about using template template parameters for this last issue but I would need to use classes) – user3770392 Jun 04 '15 at 10:37
  • 3) In theory there is not this assumption but maybe it can be relaxed, so any solution is welcomed 4) it would be quite cool if such sort of operator ( runtime-decltype ) existed because, if you had 100000 enum values for example, it would save you writing a 100000+ lines of code, replacing them with just one – user3770392 Jun 04 '15 at 10:37
  • 1
    What you're describing is just a hand-implemented version of a virtual function. What exactly is your objection to that? – user207421 Jun 04 '15 at 10:41
  • My first objection is that if I had to use standard dynamic polymorphism I would need a class for each enum value. I have to check also about any performance penalties and also about the amount of code needed to write that. Really, I don't exclude any possibilities, I am just curious to know if it is possible if another approach is feasible and better. – user3770392 Jun 04 '15 at 10:56
  • About the virtual dispatch performances, I've just found: http://stackoverflow.com/questions/4467485/vftable-performance-penalty-vs-switch-statement – user3770392 Jun 04 '15 at 11:27
  • I am familiar with this problem (and it is much worse when you want to do this with multiple enums), but I think until we get C++ reflection, you don't have great options. One is to declare the enums with something like the X Macro (http://en.wikipedia.org/wiki/X_Macro technique) so you can also generate the switch statement - that's basically doing intrusive reflection for enums. – Stefan Atev Jun 04 '15 at 13:24
  • Thanks for your understanding :) Yep, it seems quite hard at the moment but I would like to explore all the options. IMHO macros aren't necessarily evil but I prefer avoid them if I can. – user3770392 Jun 04 '15 at 13:40

3 Answers3

8

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Cheers Mate, I was implementing something similar. My only concern is now the speed in case of thousands of "cases" – user3770392 Jun 15 '15 at 16:24
  • 1
    @user3770392 The table is a static one. Your optimizer could in theory pre-calculate it (sadly, operator function pointer for a lambda is not constexpr, so I cannot mandate it). After it is created, each call is an array lookup to a jump table address. – Yakk - Adam Nevraumont Jun 15 '15 at 16:26
  • Ok, I guess I have to work out some solution without the lambda operator then :) – user3770392 Jun 16 '15 at 15:26
  • 1
    @user3770392 I do not understand that comment. "without the lambda operator" -- what lambda operator do you have to do without? The word "lambda operator" has never appeared on this page prior to your comment, so what "the" could you be referring to? – Yakk - Adam Nevraumont Jun 16 '15 at 15:38
  • I meant: "operator function pointer for a lambda ". I would like to exploit all the possible speed – user3770392 Jun 16 '15 at 15:41
  • 1
    @user3770392 sure. Just rewrite `+[](Args&&...args)->ret{ using Invoke=Z; return Invoke{}(std::forward(args)...); };` without using lambdas, and sprinkle with `constexpr`. Should be able to make a function pointer factory. Before you waste time at that, see if your compiler isn't actually pre-calculating the `static` array. – Yakk - Adam Nevraumont Jun 16 '15 at 15:43
  • Thanks for your advice, I'll try it :) – user3770392 Jun 17 '15 at 07:14
  • That is some fascinating piece of code! Thanks for that. – davidhigh Jun 19 '15 at 22:12
  • I've just tried it but it does not compile. Are you sure that this code is entirely c++11 compatible? it seems that it needs some c++14 feature – user3770392 Jun 22 '15 at 15:54
  • 2
    @user3770392 C++11 changes added; however, your original question had no mention of "C++11" or tags there. You should expect people to answer using the latest standardized version of the language, not a 4 year old variant, unless you specify. – Yakk - Adam Nevraumont Jun 22 '15 at 16:43
  • Thank you, I will try it. I guess you're right, it's just that C++14 seems not well supported – user3770392 Jun 23 '15 at 07:14
  • It still does not compile: "f was not declared in this scope" and ": error: could not convert '{{}}' from '' to 'std::array'". I'll try to fix it – user3770392 Jun 23 '15 at 08:15
  • @user3770392 I provided a link to it compiling on a C++11 compiler in my edit. What compiler are you coding on? – Yakk - Adam Nevraumont Jun 23 '15 at 10:37
  • Yep I tried that code too. I'm using gcc (SUSE Linux) 4.7.2 – user3770392 Jun 23 '15 at 10:47
  • 1
    @user3770392 gcc 4.7 only provides experimental C++11 support. In gcc 4.7.3 the first error message is about `using F`, not about `f`, and it explicitly tells you it is an unimplemented feature that is getting in the way. To work around the compiler's unimplemented feature of C++11, what I did was replace the `using F` alias with `template struct F_t { using type=R(*)(As&&...); }; template using F = typename F_t::type;` and gcc 4.7.3 compiles it now. – Yakk - Adam Nevraumont Jun 23 '15 at 11:34
  • Ok, now it compiles!! Thank you again for your support, this code rocks! the first error message was about f (lowercase) though – user3770392 Jun 24 '15 at 07:54
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
}
tahsmith
  • 1,643
  • 1
  • 17
  • 23
  • Thanks for your feedback. I would also exclude the visitor pattern because in the case of this example the whole logic is moved inside ProcessDataFn while I would like to get maximum decouplability, making everything indipendent (as far as it is possible) – user3770392 Jun 04 '15 at 11:09
  • One thing that I was thinking about is to use build something that can deduce the type of the enum I pass and use overloading to use the correct function member. I have to check this thing out – user3770392 Jun 04 '15 at 11:11
1

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

Stefan Atev
  • 485
  • 3
  • 11
  • Cheers for that. Yep, there are pro and contra using macros. About the second option you're talking about, yep, we would need to compare that approach to a standard switch. Finding a solution for this problem would be like finding the the C++ philosopher's stone so it's worth investigating it ;) – user3770392 Jun 04 '15 at 13:50
  • The performance may not be that different - depending on how the switch gets implemented, you either depend on the branch predictor to get you to the correct switch case with low overhead by predicting conditional jumps accurately, or you depend on it again to predict an indirect jump in a jump table correctly; with a virtual function you depend on the indirect jump prediction; all is hopeless if your run-time enum changes a lot; if it doesn't all solution would likely be low overhead. – Stefan Atev Jun 04 '15 at 14:08
  • Yep, we just have to make measurements and compare them but probably you're right. I recall Alexandrescu using a static map to solve a similar problem. Better go to look into that book again – user3770392 Jun 04 '15 at 14:17