Switch on Strings in C++

Environment: This is standard C++. So you do not need any special environment.

Preface

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>

// Value-Defintions of the different String values
static enum StringValue { evNotDefined, 
                          evStringValue1, 
                          evStringValue2, 
                          evStringValue3, 
                          evEnd };

// Map to associate the strings with the enum values
static std::map<std::string, StringValue> s_mapStringValues;

// User input
static char szInput[_MAX_PATH];

// Intialization
static void Initialize();

int main(int argc, char* argv[])
{
  // Init the string map
  Initialize();

  // Loop until the user stops the program
  while(1)
  {
    // Get the user's input
    cout << "Please enter a string (end to terminate): ";
    cout.flush();
    cin.getline(szInput, _MAX_PATH);
    // Switch on the value
    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

Stefan is Principal Developer at NEW LINE Software Development GmbH in Germany, focused on the development of Windows DNA and Microsoft .NET Web applications. He has published several articles about OS/2 programming and the implementation of the Singleton Design Pattern at EDM/2, and has written the doc and ppt of the chapter 'DNA to .NET Design Path' of the Microsoft .NET Readiness Kit.