为什么switch语句不能应用于字符串?


227

编译以下代码,得到的错误type illegal

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

您不能在switch或中使用字符串case。为什么?是否有任何解决方案可以很好地支持类似于打开字符串的逻辑?


6
有没有将地图构造隐藏在宏后面的增强方法?
巴尔基2012年

@balki我不确定是否可以使用boost,但是编写这样的宏很容易。如果是Qt,则可以使用QMetaEnum
phuclv

Answers:


189

之所以与类型系统有关。C / C ++并不真正支持将字符串作为一种类型。它确实支持常量char数组的概念,但是它并没有真正完全理解字符串的概念。

为了生成switch语句的代码,编译器必须了解两个值相等的含义。对于像整数和枚举这样的项目,这是一个微不足道的比较。但是编译器应该如何比较2个字符串值?区分大小写,不区分大小写,文化意识等。。。如果不完全了解字符串,则无法准确回答。

此外,C / C ++ switch语句通常作为分支表生成。为字符串样式开关生成分支表并不是那么容易。


11
分支表参数不适用-这只是编译器作者可用的一种可能方法。对于生产编译器,必须根据开关的复杂性频繁使用几种方法。
底座

5
@plinth,我把它放在那里主要是出于历史原因。编译器的历史很容易回答很多“ C / C ++为什么要这样做”的问题。在他们编写该文档时,C被赞美为汇编语言,因此switch确实是一个方便的分支表。
JaredPar 2009年

113
我投了反对票,因为我不明白编译器如何知道如何在if语句中比较2个字符串值,却忘记了在switch语句中执行相同操作的方式。

15
我认为前两段不是正确的理由。特别是从C ++ 14开始,当std::string添加文字时。这主要是历史性的。但是想到的一个问题是,按照switch当前的工作方式,必须在编译时检测到重复case的;但是,对于字符串来说,这可能并不容易(考虑运行时语言环境选择等)。我想这样的事情将需要案例,或者添加未指定的行为(从来都不是我们想要做的事情)。constexpr
MM

8
对于如何比较两个std::string值甚至std::string一个const char数组(即使用operator ==),都有明确的定义,没有技术上的原因可以阻止编译器为提供该运算符的任何类型生成switch语句。它将引发一些问题,例如标签的寿命,但所有这些主要是语言设计的决定,而不是技术上的困难。
MikeMB '17年

60

如前所述,编译器喜欢建立查找表,switch以尽可能地将语句优化为接近O(1)的时序。结合使用C ++语言没有字符串类型这一事实- std::string是标准库的一部分,而标准库本身不是语言的一部分。

我将提供您可能要考虑的替代方法,我过去一直使用它以取得良好效果。而不是切换字符串本身,而是切换使用该字符串作为输入的哈希函数的结果。如果使用预定的字符串集,则代码几乎与切换字符串一样清晰:

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:
        ...
    }
}

C编译器对switch语句所做的工作有很多显而易见的优化方法,这很有趣。


15
这确实令人失望,因为您实际上并没有进行哈希处理。使用现代C ++,实际上可以使用constexpr哈希函数在编译时进行哈希。您的解决方案看起来很干净,但是不幸的是,如果梯子继续下去,那么解决方案会令人讨厌。下面的地图解决方案会更好,并且也避免使用函数调用。此外,通过使用两个映射,您还可以内置用于错误记录的文本。
德克·贝斯特


hashit可以是constexpr函数吗?假设您传入的是const char *而不是std :: string。
维克多·斯通

但为什么?您始终可以在开关顶部使用if语句执行。两者的影响最小,但是if-else查找消除了开关的性能优势。仅使用if-else应该稍微快一些,但更重要的是,显着缩短。
佐伊

20

C ++

constexpr哈希函数:

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
}

1
您必须确保所有案例都不哈希到相同的值。即使这样,您也可能会犯一些错误,即其他哈希值散列为例如hash(“ one”)相同的字符串将错误地执行开关中的第一个“某事”。
David Ljung Madison Stellar

我知道,但是如果它的哈希值相同,就不会编译,您会及时注意到它。
尼克,

好点-但这不能解决不属于您开关的其他字符串的哈希冲突。在某些情况下,这可能并不重要,但是如果这是一个通用的“首选”解决方案,我可以想象在某个时候它是一个安全问题或类似问题。
David Ljung Madison Stellar '18

7
您可以添加operator ""使代码更漂亮。constexpr inline unsigned int operator "" _(char const * p, size_t) { return hash(p); }并像case "Peter"_: break; 演示
hare1039

15

C ++ 11更新显然不是上面的@MarmouCorp,而是http://www.codeguru.com/cpp/cpp/cpp_mfc/article.php/c4067/Switch-on-Strings-in-C.htm

使用两个映射在字符串和类枚举之间进行转换(比普通枚举更好,因为其值在其内部范围内,并且反向查找以获得不错的错误消息)。

编译器支持初始化程序列表,这意味着VS 2013 plus,可以在codeguru代码中使用static。gcc 4.8.1可以接受,不确定它可以兼容多远。

/// <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]);
}

我应该注意,我后来找到了一个需要字符串文字和编译时间计算的解决方案(我认为是C ++ 14或17),您可以在编译时对大小写字符串进行哈希处理,并在运行时对开关字符串进行哈希处理。对于很长的开关,这也许是值得的,但是如果那很重要的话,当然肯定会向后兼容。
德克·贝斯特2015年

您能在这里分享编译时的解决方案吗?谢谢!
QED

12

问题是,出于优化的原因,C ++中的switch语句仅对原始类型无效,并且只能将它们与编译时间常数进行比较。

限制的原因大概是编译器能够应用某种形式的优化将代码编译为一个cmp指令和一个goto,其中运行时根据参数的值来计算地址。由于分支和和循环在现代CPU中不能很好地发挥作用,因此这可能是重要的优化。

要解决此问题,恐怕您将不得不诉诸if语句。


可以使用可用于字符串的switch语句的优化版本。他们不能重用与原始类型相同的代码路径这一事实,并不意味着他们不能std::string与其他人成为该语言的先民,并使用有效的算法在switch语句中支持它们。
ceztko '18 -10-6

10

std::map +没有枚举的C ++ 11 lambdas模式

unordered_map对于潜在的摊销O(1)在C ++中使用HashMap的最佳方法是什么?

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

输出:

one 1
two 2
three 3
foobar -1

在方法内部使用 static

要在类中有效使用此模式,请静态初始化lambda映射,否则您将需要付费O(n)以从头开始构建它。

在这里我们可以摆脱方法变量的{}初始化static类方法中的静态变量,但我们也可以使用以下方法描述:C ++中的静态构造函数?我需要初始化私有静态对象

有必要将lambda上下文捕获[&]转换为参数,否则将无法定义该参数:const static auto lambda与按引用捕获一起使用

产生与上面相同输出的示例:

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

3
请注意,此方法与switch声明之间有区别。switch语句中的case值重复是编译时失败。使用std::unordered_map静默接受重复值。
D.Shawley,

6

在C ++和C中,开关仅适用于整数类型。改用if else梯子。C ++显然可以为字符串实现某种形式的swich语句-我猜没有人认为这值得,我同意他们的观点。


同意,但是您知道是什么使它无法使用

历史?开启实数,指针和结构(C是唯一的其他数据类型)不会带来麻烦,因此C将其限制为整数。

特别是如果您打开允许隐式转换的类,那么您将度过非常美好的时光。
Sharptooth

6

为什么不?您可以将开关实现与等效语法和相同语义一起使用。该C语言根本没有对象和字符串对象,但是in C中的字符串是指针引用的以null终止的字符串。该C++语言可以为对象比较提供重载功能或检查对象的相等性。由于C作为C++具有足够的灵活性,以对字符串这样的开关C 语言和任何类型的对象支持comparaison或支票平等C++的语言。现代C++11允许这种开关实施足够有效。

您的代码将如下所示:

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"

例如,可以使用更复杂的类型,也可以使用std::pairs任何支持等号运算的结构或类(或使用逗号进行快速运算)模式的对分)的。

特征

  • 支持比较或检查相等性的任何类型的数据
  • 建立级联嵌套开关状态器的可能性。
  • 突破或失败案例陈述的可能性
  • 使用非并发案例表达式的可能性
  • 可以通过树搜索启用快速静态/动态模式(对于C ++ 11)

语言切换的Sintax差异为

  • 大写关键字
  • CASE语句需要括号
  • 分号';' 声明末尾是不允许的
  • 不允许在CASE语句中使用冒号':'
  • 在CASE语句末尾需要BREAK或FALL关键字之一

对于C++97语言,使用线性搜索。为了C++11更现代,可以使用quick模式wuth树搜索,其中不允许CASE 中的return语句。C存在使用char*类型和零终止字符串比较的语言实现。

阅读有关此开关实现的更多信息


6

要使用最简单的容器添加变体(不需要有序的地图)...我不会为枚举而烦恼-只需将容器定义放在开关之前,这样就很容易看出哪个数字代表这种情况下。

这会在中进行散列查找,unordered_map并使用关联的内容int来驱动switch语句。应该相当快。请注意,它at已代替[],因为我已经制作了该容器const。使用[]可能会很危险-如果字符串不在地图中,则您将创建一个新的地图,并可能导致未定义结果或地图不断增长。

请注意,at()如果字符串不在地图中,则该函数将引发异常。因此,您可能要先使用进行测试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;


}

测试未定义字符串的版本如下:

const static std::unordered_map<std::string,int> string_to_case{
   {"raj",1},
   {"ben",2}
};
// in C++20, you can replace .count with .contains
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

}

4

我认为原因是正如tomjen所说,在C字符串中不是原始类型,所以在字符串中将其视为char数组,因此您不能执行以下操作:

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

3
不查找它,字符数组可能会退化为char *,而char *直接转换为整数类型。因此,它很可能会编译,但是肯定不会做您想要的。
David Thornley,2009年


2

在C ++中,您只能在int和char上使用switch语句


3
字符也变成一个整数。
斯特拉格2009年

指针也可以。这意味着您有时可以编译使用其他语言有意义的内容,但无法正常运行。
David Thornley,2009年

您实际上可以使用longlong long,而不会变成int。那里没有被截断的风险。
MSalters 2015年


0
    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;

    }

4
尽管此代码可以回答问题,但提供有关此代码为何和/或如何回答问题的其他上下文,可以提高其长期价值。
本杰明·W.

0

在许多情况下,您可以通过从字符串中提取第一个字符并将其打开来进行额外的工作。如果您的案例以相同的值开头,则可能最终不得不在charat(1)上执行嵌套开关。任何阅读您的代码的人都希望得到提示,因为大多数人可能会if-else-if


0

解决开关问题的更多功能性变通办法:

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)
    {
        // ...
    }
}


-1

开关仅适用于整数类型(int,char,bool等)。为什么不使用映射将字符串与数字配对,然后在开关上使用该数字呢?


-2

这是因为C ++将开关转换为跳转表。它对输入数据执行微不足道的操作,并且不进行比较就跳转到正确的地址。由于字符串不是数字,而是数字的数组,因此C ++无法从中创建跳转表。

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
...

(来自Wikipedia https://en.wikipedia.org/wiki/Branch_table的代码)


4
C ++不需要对其语法进行任何特定的实现。根据C ++标准,幼稚cmp/ jcc实现可能同样有效。
罗斯兰
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.