-->

Why the switch statement cannot be applied on stri

2018-12-31 16:30发布

问题:

Compiling the following code and got the error of type illegal.

int main()
{
    // Compilation error - switch expression of type illegal
    switch(std::string(\"raj\"))
    {
    case\"sda\":
    }
}

You cannot use string in either switch or case. Why? Is there any solution that works nicely to support logic similar to switch on strings?

回答1:

The reason why has to do with the type system. C/C++ doesn\'t really support strings as a type. It does support the idea of a constant char array but it doesn\'t really fully understand the notion of a string.

In order to generate the code for a switch statement the compiler must understand what it means for two values to be equal. For items like ints and enums, this is a trivial bit comparison. But how should the compiler compare 2 string values? Case sensitive, insensitive, culture aware, etc ... Without a full awareness of a string this cannot be accurately answered.

Additionally, C/C++ switch statements are typically generated as branch tables. It\'s not nearly as easy to generate a branch table for a string style switch.



回答2:

As mentioned previously, compilers like to build lookup tables that optimize switch statements to near O(1) timing whenever possible. Combine this with the fact that the C++ Language doesn\'t have a string type - std::string is part of the Standard Library which is not part of the Language per se.

I will offer an alternative that you might want to consider, I\'ve used it in the past to good effect. Instead of switching over the string itself, switch over the result of a hash function that uses the string as input. Your code will be almost as clear as switching over the string if you are using a predetermined set of strings:

enum string_code {
    eFred,
    eBarney,
    eWilma,
    eBetty,
    ...
};

string_code hashit (std::string const& inString) {
    if (inString == \"Fred\") return eFred;
    if (inString == \"Barney\") return eBarney;
    ...
}

void foo() {
    switch (hashit(stringValue)) {
    case eFred:
        ...
    case eBarney:
        ...
    }
}

There are a bunch of obvious optimizations that pretty much follow what the C compiler would do with a switch statement... funny how that happens.



回答3:

You can only use switch on primitive such as int, char and enum. The easiest solution to do it like you want to, is to use an enum.

#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;
}

Code written by Stefan Ruck on July 25th, 2001.



回答4:

C++ 11 update of apparently not @MarmouCorp above but http://www.codeguru.com/cpp/cpp/cpp_mfc/article.php/c4067/Switch-on-Strings-in-C.htm

Uses two maps to convert between the strings and the class enum (better than plain enum because its values are scoped inside it, and reverse lookup for nice error messages).

The use of static in the codeguru code is possible with compiler support for initializer lists which means VS 2013 plus. gcc 4.8.1 was ok with it, not sure how much farther back it would be compatible.

/// <summary>
/// Enum for String values we want to switch on
/// </summary>
enum class TestType
{
    SetType,
    GetType
};

/// <summary>
/// Map from strings to enum values
/// </summary>
std::map<std::string, TestType> MnCTest::s_mapStringToTestType =
{
    { \"setType\", TestType::SetType },
    { \"getType\", TestType::GetType }
};

/// <summary>
/// Map from enum values to strings
/// </summary>
std::map<TestType, std::string> MnCTest::s_mapTestTypeToString
{
    {TestType::SetType, \"setType\"}, 
    {TestType::GetType, \"getType\"}, 
};

...

std::string someString = \"setType\";
TestType testType = s_mapStringToTestType[someString];
switch (testType)
{
    case TestType::SetType:
        break;

    case TestType::GetType:
        break;

    default:
        LogError(\"Unknown TestType \", s_mapTestTypeToString[testType]);
}


回答5:

The problem is that for reasons of optimization the switch statement in C++ does not work on anything but primitive types, and you can only compare them with compile time constants.

Presumably the reason for the restriction is that the compiler is able to apply some form of optimization compiling the code down to one cmp instruction and a goto where the address is computed based on the value of the argument at runtime. Since branching and and loops don\'t play nicely with modern CPUs, this can be an important optimization.

To go around this, I am afraid you will have to resort to if statements.



回答6:

std::map + C++11 lambdas pattern without enums

unordered_map for the potential amortized O(1): What is the best way to use a HashMap in C++?

#include <functional>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

int main() {
    int result;
    const std::unordered_map<std::string,std::function<void()>> m{
        {\"one\",   [&](){ result = 1; }},
        {\"two\",   [&](){ result = 2; }},
        {\"three\", [&](){ result = 3; }},
    };
    const auto end = m.end();
    std::vector<std::string> strings{\"one\", \"two\", \"three\", \"foobar\"};
    for (const auto& s : strings) {
        auto it = m.find(s);
        if (it != end) {
            it->second();
        } else {
            result = -1;
        }
        std::cout << s << \" \" << result << std::endl;
    }
}

Output:

one 1
two 2
three 3
foobar -1

Usage inside methods with static

To use this pattern efficiently inside classes, initialize the lambda map statically, or else you pay O(n) every time to build it from scratch.

Here we can get away with the {} initialization of a static method variable: Static variables in class methods , but we could also use the methods described at: static constructors in C++? I need to initialize private static objects

It was necessary to transform the lambda context capture [&] into an argument, or that would have been undefined: const static auto lambda used with capture by reference

Example that produces the same output as above:

#include <functional>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

class RangeSwitch {
public:
    void method(std::string key, int &result) {
        static const std::unordered_map<std::string,std::function<void(int&)>> m{
            {\"one\",   [](int& result){ result = 1; }},
            {\"two\",   [](int& result){ result = 2; }},
            {\"three\", [](int& result){ result = 3; }},
        };
        static const auto end = m.end();
        auto it = m.find(key);
        if (it != end) {
            it->second(result);
        } else {
            result = -1;
        }
    }
};

int main() {
    RangeSwitch rangeSwitch;
    int result;
    std::vector<std::string> strings{\"one\", \"two\", \"three\", \"foobar\"};
    for (const auto& s : strings) {
        rangeSwitch.method(s, result);
        std::cout << s << \" \" << result << std::endl;
    }
}


回答7:

In C++ and C switches only work on integer types. Use an if else ladder instead. C++ could obviously have implemented some sort of swich statement for strings - I guess nobody thought it worthwhile, and I agree with them.



回答8:

Why not? You can use switch implementation with equivalent syntax and same semantics. The C language does not have objects and strings objects at all, but strings in C is null terminated strings referenced by pointer. The C++ language have possibility to make overload functions for objects comparision or checking objects equalities. As C as C++ is enough flexible to have such switch for strings for C language and for objects of any type that support comparaison or check equality for C++ language. And modern C++11 allow to have this switch implementation enough effective.

Your code will be like this:

std::string name = \"Alice\";

std::string gender = \"boy\";
std::string role;

SWITCH(name)
  CASE(\"Alice\")   FALL
  CASE(\"Carol\")   gender = \"girl\"; FALL
  CASE(\"Bob\")     FALL
  CASE(\"Dave\")    role   = \"participant\"; BREAK
  CASE(\"Mallory\") FALL
  CASE(\"Trudy\")   role   = \"attacker\";    BREAK
  CASE(\"Peggy\")   gender = \"girl\"; FALL
  CASE(\"Victor\")  role   = \"verifier\";    BREAK
  DEFAULT         role   = \"other\";
END

// the role will be: \"participant\"
// the gender will be: \"girl\"

It is possible to use more complicated types for example std::pairs or any structs or classes that support equality operations (or comarisions for quick mode).

Features

  • any type of data which support comparisions or checking equality
  • possibility to build cascading nested switch statemens.
  • possibility to break or fall through case statements
  • possibility to use non constatnt case expressions
  • possible to enable quick static/dynamic mode with tree searching (for C++11)

Sintax differences with language switch is

  • uppercase keywords
  • need parentheses for CASE statement
  • semicolon \';\' at end of statements is not allowed
  • colon \':\' at CASE statement is not allowed
  • need one of BREAK or FALL keyword at end of CASE statement

For C++97 language used linear search. For C++11 and more modern possible to use quick mode wuth tree search where return statement in CASE becoming not allowed. The C language implementation exists where char* type and zero-terminated string comparisions is used.

Read more about this switch implementation.



回答9:

In C++ you can only use a switch statement on int and char



回答10:

I think the reason is that in C strings are not primitive types, as tomjen said, think in a string as a char array, so you can not do things like:

switch (char[]) { // ...
switch (int[]) { // ...


回答11:

C++

constexpr hash function:

constexpr unsigned int hash(const char *s, int off = 0) {                        
    return !s[off] ? 5381 : (hash(s, off+1)*33) ^ s[off];                           
}                                                                                

switch( hash(str) ){
case hash(\"one\") : // do something
case hash(\"two\") : // do something
}


回答12:

In c++ strings are not first class citizens. The string operations are done through standard library. I think, that is the reason. Also, C++ uses branch table optimization to optimize the switch case statements. Have a look at the link.

http://en.wikipedia.org/wiki/Switch_statement



回答13:

To add a variation using the simplest container possible (no need for an ordered map)... I wouldn\'t bother with an enum--just put the container definition immediately before the switch so it\'ll be easy to see which number represents which case.

This does a hashed lookup in the unordered_map and uses the associated int to drive the switch statement. Should be quite fast. Note that at is used instead of [], as I\'ve made that contained const. Using [] can be dangerous--if the string isn\'t in the map, you\'ll create a new mapping and may end up with undefined results or a continuously growing map.

Note that the at() function will throw an exception if the string isn\'t in the map. So you may want to test first using count().

const static std::unordered_map<std::string,int> string_to_case{
   {\"raj\",1},
   {\"ben\",2}
};
switch(string_to_case.at(\"raj\")) {
  case 1: // this is the \"raj\" case
       break;
  case 2: // this is the \"ben\" case
       break;


}

The version with a test for an undefined string follows:

const static std::unordered_map<std::string,int> string_to_case{
   {\"raj\",1},
   {\"ben\",2}
};
switch(string_to_case.count(\"raj\") ? string_to_case.at(\"raj\") : 0) {
  case 1: // this is the \"raj\" case
       break;
  case 2: // this is the \"ben\" case
       break;
  case 0: //this is for the undefined case

}


回答14:

You can\'t use string in switch case.Only int & char are allowed. Instead you can try enum for representing the string and use it in the switch case block like

enum MyString(raj,taj,aaj);

Use it int the swich case statement.



回答15:

Switches only work with integral types (int, char, bool, etc.). Why not use a map to pair a string with a number and then use that number with the switch?



回答16:

    cout << \"\\nEnter word to select your choice\\n\"; 
    cout << \"ex to exit program (0)\\n\";     
    cout << \"m     to set month(1)\\n\";
    cout << \"y     to set year(2)\\n\";
    cout << \"rm     to return the month(4)\\n\";
    cout << \"ry     to return year(5)\\n\";
    cout << \"pc     to print the calendar for a month(6)\\n\";
    cout << \"fdc      to print the first day of the month(1)\\n\";
    cin >> c;
    cout << endl;
    a = c.compare(\"ex\") ?c.compare(\"m\") ?c.compare(\"y\") ? c.compare(\"rm\")?c.compare(\"ry\") ? c.compare(\"pc\") ? c.compare(\"fdc\") ? 7 : 6 :  5  : 4 : 3 : 2 : 1 : 0;
    switch (a)
    {
        case 0:
            return 1;

        case 1:                   ///m
        {
            cout << \"enter month\\n\";
            cin >> c;
            cout << endl;
            myCalendar.setMonth(c);
            break;
        }
        case 2:
            cout << \"Enter year(yyyy)\\n\";
            cin >> y;
            cout << endl;
            myCalendar.setYear(y);
            break;
        case 3:
             myCalendar.getMonth();
            break;
        case 4:
            myCalendar.getYear();
        case 5:
            cout << \"Enter month and year\\n\";
            cin >> c >> y;
            cout << endl;
            myCalendar.almanaq(c,y);
            break;
        case 6:
            break;

    }


回答17:

in many cases you can avid extra work by pulling the first char from the string and switching on that. may end up having to do a nested switch on charat(1) if your cases start with the same value. anyone reading your code would appreciate a hint though because most would prob just if-else-if



回答18:

More functional workaround to the switch problem:

class APIHandlerImpl
{

// define map of \"cases\"
std::map<string, std::function<void(server*, websocketpp::connection_hdl, string)>> in_events;

public:
    APIHandlerImpl()
    {
        // bind handler method in constructor
        in_events[\"/hello\"] = std::bind(&APIHandlerImpl::handleHello, this, _1, _2, _3);
        in_events[\"/bye\"] = std::bind(&APIHandlerImpl::handleBye, this, _1, _2, _3);
    }

    void onEvent(string event = \"/hello\", string data = \"{}\")
    {
        // execute event based on incomming event
        in_events[event](s, hdl, data);
    }

    void APIHandlerImpl::handleHello(server* s, websocketpp::connection_hdl hdl, string data)
    {
        // ...
    }

    void APIHandlerImpl::handleBye(server* s, websocketpp::connection_hdl hdl, string data)
    {
        // ...
    }
}


回答19:

That\'s because C++ turns switches into jump tables. It performs a trivial operation on the input data and jumps to the proper address without comparing. Since a string is not a number, but an array of numbers, C++ cannot create a jump table from it.

movf    INDEX,W     ; move the index value into the W (working) register from memory
addwf   PCL,F       ; add it to the program counter. each PIC instruction is one byte
                    ; so there is no need to perform any multiplication. 
                    ; Most architectures will transform the index in some way before 
                    ; adding it to the program counter

table                   ; the branch table begins here with this label
    goto    index_zero  ; each of these goto instructions is an unconditional branch
    goto    index_one   ; of code
    goto    index_two
    goto    index_three

index_zero
    ; code is added here to perform whatever action is required when INDEX = zero
    return

index_one
...

(code from wikipedia https://en.wikipedia.org/wiki/Branch_table)