How many times have you written code like this:
if (!strcmp(pszValue, "Value X"))
DoThis();
else if (!strcmp(pszValue, "Value Y"))
DoThat();
else if (!strcmp(pszValue, "Value Z"))
DoSomethingElse();
else
DontKnowWhatToDo();
Too many! And those of you who had the chance to take a look
at C# might think 'Why can't I code something similar in C++ like this:
switch(strValue)
{
case "Value X":
DoThis();
break;
case "Value Y":
DoThat();
break;
case "Value Z";
DoSomethingElse();
break;
default:
DontKnowWhatToDo();
break;
}
(This code is legal C#. You can switch on strings in C#.)
In this article I will show you a way to implement a switch on
strings using pure standard C++.
The Bad News
I can't give you a solution to do exactly what you can do in C#.
The Good News
The Standard Template Library (STL), part of the ANSI/ISO
C++ Standard, offers everything needed to get really close to the C# sample.
The solution is very simple. You need an enumeration and a std::map,
and that's it. The enumeration defines the numeric values use in the
switch statement. The std::map contains the link between the valid
string values you want to compare some runtime data against, and the numeric enum
values you can make a switch on. The string is the key of the map, the
enumerator the value.
Here We Go
After thinking about the problem a little bit, there is only
one place where to put the enum definition and the std::map
definition: The .cpp file. Both, the enum and the std::map,
should be declared as static to make them visible only to the code of this
particular .cpp file, which helps to avoid global namespace pollution (for more
on this see John Lakos, Large Scale C++ Software Design, Addison-Wesley). There
is no need to declare them as members of any class since normally the connection
between a string value and the action to be taken is very specific to the
respective code.
And here comes a sample Switch On String implementation I will use for later
discussion:
#include <map>
#include <string>
#include <iostream.h>
static enum StringValue { evNotDefined,
evStringValue1,
evStringValue2,
evStringValue3,
evEnd };
static std::map<std::string, StringValue> s_mapStringValues;
static char szInput[_MAX_PATH];
static void Initialize();
int main(int argc, char* argv[])
{
Initialize();
while(1)
{
cout << "Please enter a string (end to terminate): ";
cout.flush();
cin.getline(szInput, _MAX_PATH);
switch(s_mapStringValues[szInput])
{
case evStringValue1:
cout << "Detected the first valid string." << endl;
break;
case evStringValue2:
cout << "Detected the second valid string." << endl;
break;
case evStringValue3:
cout << "Detected the third valid string." << endl;
break;
case evEnd:
cout << "Detected program end command. "
<< "Programm will be stopped." << endl;
return(0);
default:
cout << "'" << szInput
<< "' is an invalid string. s_mapStringValues now contains "
<< s_mapStringValues.size()
<< " entries." << endl;
break;
}
}
return 0;
}
void Initialize()
{
s_mapStringValues["First Value"] = evStringValue1;
s_mapStringValues["Second Value"] = evStringValue2;
s_mapStringValues["Third Value"] = evStringValue3;
s_mapStringValues["end"] = evEnd;
cout << "s_mapStringValues contains "
<< s_mapStringValues.size()
<< " entries." << endl;
}
In looking at the enum definition, you see that the first
value is evNotDefined (ev means 'enum value'). Why this?
std::map::operator[] can be used for two things: To set a value of a
key (as you can see in Initialize()) and to retrieve the value
associated with it (look at the switch statement in the main()
function). The most important sentence of its description is: 'If this
element (key the value [author]) does not exist, it is inserted.'. In
relation to our sample above this means, any time we go thru the switch
and the value of szInput is new to s_mapStringValues, it is
inserted (this is why I added the print out of the map's size) and the value of
the new element is set to the initial value of an integral type, which is 0 by
default. And as long as there is no special value assigned to the first
enumerator of an enumeration, its value is zero too (please refer to the C++
Standard for more on this). That means, if you start the enumeration with a valid
value (here: evStringValue1), any unknown (and so unexpected) string
value would lead to a valid case of the switch, but to an
invalid program behaviour. That's why evNotDefined is the first
enumerator.
In case you have to use a given enumeration where an enumerator
with value zero is defined, you should call std::map::find() before the
switch statement to check if the string value is valid.
The rest of the sample is quite simple. The switch is not
made on the string itself but on the numeric value associated to it by the
std::map. So there's not really magic in there.
When to Initialize
In this sample, the answer is easy: When the program
starts. But unfortunately real life applications aren't as simple as all these
samples. So when to initialize in real life?
There are two scenarios we have to cover: The switch is part of a non-static
member function, and the switch is part of a static method or global function.
If it is part of a non-static member function, the class' constructor should
do initialization, as long as you do not have to care about the runtime
behaviour of the ctor. (See comment on 'lazy' init below.) To avoid useless
multiple initialization, one should check the size of the map before setting the
value. If it is zero, no initialization has been made before, otherwise there is
no more need for it.
In case of a static method or global function, you would prefer something
similar to C#'s static constructor. But there is nothing like this in C++ (as
far as I know). So you do have two choices: 'Lazy' or 'As-soon-as-possible'
initialization.
- 'Lazy' initialization means just before the map is used, a
check is done to see if it is filled correctly (again, check its size), and, if not, you will do
so.
- 'As-soon-as-possible' init needs an additional Init
method the (class) user has to call in order make sure the switch will work
correctly.
Both solutions have their pros and cons. The 'lazy' way leads to
additional code before every switch that is using the map and so has an
impact on the runtime behaviour. (You can't tell exactly how long the call will
take since you do not know whether the string map will be initialized or not.)
But the user can't forget to initialize the map. The 'As-soon-as-possible'
implies the risk the user has forgotten to call the Init method (but this will
be noticed very soon as long as you do some testing on you app), but saves some
code when using the map and guaranties runtime behaviour. So it's up to you
which way to go. I do prefer the latter one.
Case In-Sensitive
As long as you do not have to use special characters
like umlaute (d, |, ...) or whatever, you just have to fill the map with upper-
or lower-case-only strings and use _strupr or _strlwr in the switch
statement.
A solution for special characters might be a good exercise for the ambitioned
reader.
Conclusion
I think this solution is a good sample to show the power of
the STL. Hopefully it was able to convince you of the simplicity and elegance of
switch statements on string values. Use the STL as much as you can.
Remarks
I hope you do believe me when I say that I worked this out
without knowing about the discussions on this topic at comp.lang.c++.moderated
or any other discussion group. I have to confess that I did not read thru this
discussions since I'm really happy with the solution demonstrated, which has
proven its usability in real life. But I do not want to give the impression that
I was the first and only one who had the idea of using a map to implement
switch on strings in C++.
About the Author