Introduction
Sometimes we have a series of if-then statements in our code where a switch statement would be clearer. C++ doesn’t support switching on non-integral values and so we end up with a series of if-then statements instead. We might also not bother to optimize those if-then statements (utilizing a map, hash-table or other quick lookup data structure) because we don’t deem those parts of the code performance critical. In such places, it might be clearer to the maintainer of the code to have something akin to a switch statement instead.
In order to improve readability, we can use a fluent interface via method chaining. A macro based solution would be another approach, but it would be harder to debug and we wouldn’t get code completion.
Design
Let’s think of what syntax we would like to have and work towards it.
- We want to be able to switch on any object which has operator equal to, defined.
- We don't want case expressions to be restricted to compile time constants.
- We want to be able to fall through implicitly and break explicitly.
- Finally, we want to have a default.
The following code illustrates the features we want:
int main()
{
cout << "day?" << endl;
string day;
cin >> day;
Switch(day)
.Case("monday")
.Case("tuesday")
.Case("wednesday")
.Case("thursday")
.Case("friday", [] { cout << "weekday" << endl; }).Break()
.Case("sunday")
.Case("saturday", [] { cout << "weekend" << endl; }).Break()
.Default([] { cout << "unknown day" << endl; });
return 0;
}
Implementation
Now that we’ve decided how it should look in action, we can think about the implementation itself. We would need to have a templated function, Switch
. Further, it would return an object which has the functions Case
, Break
and Default
. These functions would mutate the object and then return a reference to it, so that the functions can be chained.
Turns out, it’s quite easy to implement:
#pragma once
#include <functional>
template <class T>
class CaseBreakDefault
{
public:
typedef std::function<void ()> Block;
private:
const T &m_value;
bool m_match;
bool m_break;
public:
explicit CaseBreakDefault(const T &value) :
m_value(value),
m_match(false),
m_break(false)
{
}
CaseBreakDefault<T> &Case(const T &value, const Block &block)
{
if( m_break )
return *this;
if( m_value == value )
m_match = true;
if( m_match && block)
block();
return *this;
}
CaseBreakDefault<T> &Case(const T &value)
{
return Case(value, nullptr);
}
CaseBreakDefault<T> &Break()
{
if( m_match )
m_break = true;
return *this;
}
void Default(const Block &block)
{
if( m_break )
return;
if( block )
block();
}
private:
void operator =(const CaseBreakDefault &);
};
template <class T>
CaseBreakDefault<T> Switch(const T &value)
{
return CaseBreakDefault<T>(value);
}
A no-nonsense interface
We can improve the design of the interface however, to restrict it from inadvertently being used illogically. We don’t want to allow a break statement to immediately follow the switch statement or another break statement. To this end, we will derive another class from CaseBreakDefault
which hides the break statement. This derived class called CaseDefault
(for all the right reasons) would be the type of object returned from the Switch
and Break
functions.
The final code (with the changes in bold) is shown below for completeness.
#pragma once
#include <functional>
template <class T>
class CaseDefault;
template <class T>
class CaseBreakDefault
{
public:
typedef std::function<void ()> Block;
private:
const T &m_value;
bool m_match;
bool m_break;
public:
explicit CaseBreakDefault(const T &value) :
m_value(value),
m_match(false),
m_break(false)
{
}
CaseBreakDefault<T> &Case(const T &value, const Block &block)
{
if( m_break )
return *this;
if( m_value == value )
m_match = true;
if( m_match && block)
block();
return *this;
}
CaseBreakDefault<T> &Case(const T &value)
{
return Case(value, nullptr);
}
CaseDefault<T> &Break()
{
if( m_match )
m_break = true;
return static_cast<CaseDefault<T> &>(*this);
}
void Default(const Block &block)
{
if( m_break )
return;
if( block )
block();
}
private:
void operator =(const CaseBreakDefault &);
};
template <class T>
class CaseDefault : public CaseBreakDefault<T>
{
public:
explicit CaseDefault(const T &value) :
CaseBreakDefault(value)
{
}
private:
using CaseBreakDefault::Break;
};
template <class T>
CaseDefault<T> Switch(const T &value)
{
return CaseDefault<T>(value);
}
Another example
Let’s say you’re making a game where the player controls his character using the keyboard. You might want the keys which control the character to be configured by the player at runtime or come from a configuration file. The following code illustrates how this might look:
int playerX, playerY;
int leftCode, rightCode, upCode, downCode;
void OnKeyPressed(const int code)
{
Switch(code)
.Case(leftCode, [] { --playerX; }).Break() .Case(rightCode, [] { ++playerX; }).Break() .Case(upCode, [] { --playerY; }).Break() .Case(downCode, [] { ++playerY; }).Break(); }
Notes
The performance of such a simple implementation is linear in time. For a relatively small number of case statements, its performance might not matter. For a larger number of case statements, an appropriately tuned data structure should be considered instead.
Improvements
Some ideas that could be experimented with:
- Break implicitly and fall through explicitly via a
Fall
function. This would deviate from the native switch statement syntax, but could help in preventing errors. - Accept a user-defined binary predicate for performing comparisons. The switch statement could take this predicate as a second parameter, defaulting to using operator equal to, if none were specified.
- Use a binary search instead of a linear search to improve performance. Of course this would mean that the case expressions would need to be sorted.
Closing
The source code has been tested with Microsoft and Intel C++ compilers.
There is much potential for improvement; if you make changes to the code, improve it, or have some better ideas, I would love to know. I can be reached by email at francisxavierjp [at] gmail [dot] com. Comments and suggestions are always welcome!