The switch statement is a powerful construct in C that allows you to clearly express complex if/else if/else blocks as a single statement. By providing this construct, you can write code that’s easier to understand and maintain. The downside to this is that you must use constant values for the case statements, and these constants must sensibly compare using the == operator. So, for instance, it is possible to have integers in your case statements, but not possible to have strings. Attempting to compare strings with == will not work reliably due to pointer comparisons. However, there is a solution that has the power and clarity of a switch statement in C++, and thanks to lambda functions in C++11, it can be elegant as well.
At its core, a switch statement is really just a bunch of if/else statements, where the operand to switch is one side of the equality test, and the case statement operand is the other side of the equality test. For instance:
int i; switch (i) { case 1: break; case 2: break; case 3: case 4: break; default: break; } if (i == 1) { } else if (i == 2) { } else if (i == 3 || i == 4) { } else { }
These two constructs are basically equivalent in terms of their semantics. So our goal is to come up with a class-based switch that provides the same clarity. Namely, it needs to be able to support a single case statement, multiple fall-through case statements, and a default statement. What would the prototype for such a class look like: Well, the constructor should take the operand from switch, and there should be a function to represent which receives a parameter for the case operand, and a function to execute if the case is true. There should also be a function to represent the default clause, which takes a function to execute as well. The “case” functions should only execute the code block if the case operand is true, and the default function should only execute the code block if none of the case operands are true. And it should provide no worse performance than the if/else moral equivalent. Sounds like a challenge!
I must admit that the basic idea I’m pitching here is not of my own creation. While working on LLVM, I ran across this construct and fell in love with it. However, I have modified it slightly to generalize it a bit more.
The idea is to chain together calls on a common object — each call returns an instance of the class itself. Then you can store state information on the class, used to determine the ultimate result. The basic structure we’ll end up with will look something like:
Switch s( operand ); s.Case( case1, ??? ) .Case( case2, ??? ) .Default( ??? );
Let’s start with an easy test case — one that switches on an integer value, and returns a string value. This should be enough to give an understanding of the mechanics.
class IntegerSwitch { int mOperand; const std::string *mResult; public: explicit IntegerSwitch( int i ) : mOperand( i ), mResult( NULL ) {} IntegerSwitch& Case( int i, const std::string& output ) { if (!mResult && i == mOperand) { mResult = &output; } return *this; } std::string Default( const std::string& output ) { if (!mResult) { return output; } else { return *mResult; } } }; // Usage IntegerSwitch intSwitch( 42 ); std::string results = intSwitch .Case( 12, "The result was 12" ) .Case( 33, "The result was 33" ) .Case( 42, "42 is the answer to the meaning of life" ) .Default( "I don't know what the result was" );
There’s a fair amount going on here, but the basic idea is quite simple. The constructor accepts the switch statement operand. This is the thing being switched on. There’s a Case function which accepts the case statement operand. This is the thing being compared against. The Case function also accepts the results for that case — if the operands match, then the result of the switch is this parameter. The Case function first checks to see if a result has already been obtained, and if one has, there’s no need to go further. If a result has not been obtained, then there’s an equality test to see if the operands match. If they do, a pointer to the result is stored off on the switch class itself. The Default function checks whether the result has been found or not, and if it has not been found, it returns its parameter. If the result has been found, it returns that result.
Most of the magic is wound up in the fact that the Case function returns a reference to the switch class itself. In this fashion, the caller is able to chain these calls together, and still keep stateful information on the class. Once the first Case statement succeeds, subsequent ones are not considered, nor is the default. If none of the Case statements succeed, then the Default function returns its parameter. However, there are a few drawbacks to this class. What if the caller does not want a default statement? And what if the caller wants some fall-through cases? Thankfully, these are also easy to accomplish.
The ability to elide a Default function can be accomplished by implementing a conversion operator from IntegerSwitch to std::string. In this way, the caller can simply assign the final Case function results to std::string. However, the danger in this is how to handle when mResult is NULL? In that case, I believe an assert is acceptable — if the caller assumes all of the cases are covered, and they are not covered in actuality, there’s possibly a bug.
operator std::string() const { assert( mResult && "There is no default clause, but the results are undefined" ); return *mResult; }
Having multiple fall-through cases is a bit more challenging. It is easy to solve the case of 2, 3 or even 4 fall-through cases. You can do the trivial solution by creating a Cases function which accepts multiple integer parameters, and only one std::string output parameter. Then simply chain calls to Case together.
IntegerSwitch& Cases( int i, int j, const std::string& output ) { return Case( i, output ).Case( j, output ); }
However, solving this problem for N cases is rather difficult. Using varargs may immediately come to mind, but the problem is in determining where the final parameter falls, not to mention the lack of type-safety. It might be possible to use C++11’s variadic templates to form a parameter pack to solve this, but I suspect there will be difficulty in keeping the signature consistent with the Case function (where the output is the last parameter). I’d be interested in seeing a working solution using variadic templates if someone gets one working.
Now that we’ve seen how to handle a switch statement mapping integer cases to string values, let’s take it a step further. Ultimately, a switch statement should be able to execute arbitrary code when a case has been hit. And we should be able to evaluate arbitrary expressions for the cases instead of integers! The way we generalize the solution is to use templates.
template <typename T, typename R> class ExpressionSwitch { T mOperand; const R *mResult; public: explicit ExpressionSwitch( const T& operand ) : mOperand( operand ), mResult( NULL ) {} ExpressionSwitch& Case( const T& input, const R& output ) { if (!mResult && mOperand == input) { mResult = &output; } return *this; } R Default( const R& output ) const { if (mResult) { return *mResult; } else { return output; } } operator R() const { assert( mResult && "Fell off the end of the switch without a default" ); return *mResult; } };
As you can see, the solution looks remarkably similar to the one we came up with for integers to strings. Except in this case, we’re mapping an arbitrary type T to an arbitrary value R. The only caveat to this code is that T needs to be comparable using operator==.
Using this new class, we can replicate the integer to string behavior:
ExpressionSwitch<int, std::string> expSwitch( 42 ); std::string results = expSwitch .Case( 12, "The result was 12" ) .Case( 33, "The result was 33" ) .Case( 42, "42 is the answer to the meaning of life" ) .Default( "I don't know what the result was" );
But we can also use it to evaluate expression and execute arbitrary code using C++11’s lambda functions.
ExpressionSwitch<std::string, std::function<void(void)>> test("Bar"); test .Case("Foo", [] { ::printf( "Foo was the case\n" ); }) .Case("Bar", [] { ::printf( "Bar was the case\n" ); }) .Case("Testing", [] { ::printf( "Testing was the case\n" ); }) .Default([] { ::printf( "We could not find the case\n" ); }) ();
If you run this code, you will see “Bar was the case\n” printed to your console. So we’ve accomplished what we set out to do — we have a switch statement that evaluates expressions for the cases, and can run arbitrary code when a case is matched (or default is matched). But there’s one last thing we can do to add even more power to this approach — let’s allow for custom comparison predicates.
First, our class template declaration should allow for the predicate. Then we need to keep a predicate instance on the class, and call it from the Case function:
template <typename T, typename R, class Pred = std::equal_to<T>> class ExpressionSwitch { T mOperand; const R *mResult; const Pred P; public: explicit ExpressionSwitch( const T& operand ) : mOperand( operand ), mResult( NULL ) {} ExpressionSwitch& Case( const T& input, const R& output ) { if (!mResult && P(mOperand, input)) { mResult = &output; } return *this; } R Default( const R& output ) const { if (mResult) { return *mResult; } else { return output; } } operator R() const { assert( mResult && "Fell off the end of the switch without a default" ); return *mResult; } };
This allows us to pass in our own predicate to the switch, for instance:
struct str_pred { bool operator()(const std::string& l, const std::string& r) const { return (l.length() == r.length()) && (l == r); } }; ExpressionSwitch<std::string, std::function<void(void)>, struct str_pred> test("Bar"); test .Case("Foo", [] { ::printf( "Foo was the case\n" ); }) .Case("Bar", [] { ::printf( "Bar was the case\n" ); }) .Case("Testing", [] { ::printf( "Testing was the case\n" ); }) .Default([] { ::printf( "We could not find the case\n" ); }) ();
With that, our ExpressionSwitch class is complete. I would like to thank the LLVM community for giving me the original idea! If you have ideas of how to improve the design, or have questions about how it works, please leave comments!
Thank you for the instructive example. By working through my compiler has found a syntax error in the last template, line 5: const Pred P;
const makes no sense because we have a class definition. Nevertheless, in troubleshooting you learn even more:-)