Something that used to bug me—used to, because I am so accustomed to work around it that I rarely notice the problem—is that in neither C nor C++ you can use strings ( const char * or std::string ) in switch/case statement. Indeed, the switch/case statement works only on integral values (an enum , an integral type such as char and int , or an object type with implicit cast to an integral type). But strings aren’t of integral types!

In pure C, we’re pretty much done for. The C preprocessor is too weak to help us built compile-time expression out of strings (or, more exactly, const char * ), and there’sn’t much else in the language to help us. However, things are a bit different in C++.

Since C++11, a function or a variable can be defined as constexpr , that is, guaranteed to have value entirely defined at compile-time. This allows the compiler to compute, at compile time, the values of expressions. (This, in turn, implies that the code “compiles away” and just leaves the result.) There are a number of requirements to make a function or an expression constexpr , but we can still use that to make switch/case work on strings (or const char * ).

The switch/case statement itself will still require an integral operand, so we must transform a string into an integral value. The usual way of doing this is to use a hash function. Can we built a constexpr hash function?

Well, yes, obviously—otherwise I wouldn’t have bothered to write that much text. One limitation of constexpr functions is that they pretty much have to limit themselves to declarations, directives, and a return statement. A good way of using these limitations is to think functional programming, where recursion is the main control mechanism. To hash a string (or a const char * ) we must scan every character at least once. This suggest a hash function such as:

uint64_t constexpr mix(char m, uint64_t s) { return ((s<<7) + ~(s>>3)) + ~m; } uint64_t constexpr hash(const char * m) { return (*m) ? mix(*m,hash(m+1)) : 0; }

…where mix is the real hash function. I’m sure you can come up with something better, but as long as it is good enough, it’ll work just fine. So how do we use that in a switch/case? Most simply:

void switch_test(const char * str) { switch( hash(str) ) // run-time { case hash("tatatututoto"): // compile-time std::cout << "tutu!" << std::endl; break; case hash("tatatititoto"): // compile-time std::cout << "titi!" << std::endl; break; default: std::cout << "wut?" << std::endl; break; }; } int main() { switch_test("tatatititoto"); switch_test("tatatututoto"); switch_test("abababubabub"); return 0; }

This indeed produces the desired output:

titi! tutu! wut?

*

* *

The main problem with this approach is the possibility of hash collisions. The above hash function isn’t very strong, very obviously, but can be dealt with by the compiler. If a collision occurs, it will be detected at compile-time and the compiler will issue a warning or an error about “duplicate case value”. In this case, the only correct workaround is to eliminate collisions by changing the hash function. Anyway, I’m sure you can do better than the mix hash function above.

Share this: Reddit

Twitter

More

Facebook

Email



Like this: Like Loading... Related