Почему оператор switch нельзя применять к строкам?


227

Компилируя следующий код и получил ошибку type illegal.

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

Вы не можете использовать строку в switchили case. Зачем? Есть ли какое-нибудь решение, которое хорошо работает для поддержки логики, похожей на включение строк?


6
Есть ли альтернатива повышения, которая скрывает построение карты, перечисление за MACRO?
Балки

@balki Я не уверен насчет надстройки, но такие макросы легко написать. В случае Qt вы можете скрыть отображение с помощьюQMetaEnum
phuclv

Ответы:


189

Причина, почему это связано с системой типов. C / C ++ не поддерживает строки как тип. Он поддерживает идею константного массива char, но не совсем понимает понятие строки.

Чтобы сгенерировать код для оператора switch, компилятор должен понимать, что означает, что два значения равны. Для таких элементов, как int и enums, это тривиальное сравнение битов. Но как компилятор должен сравнивать 2 строковых значения? Чувствителен к регистру, нечувствителен, осведомлен о культуре и т. Д. Без полного понимания строки невозможно дать точный ответ.

Кроме того, операторы переключения C / C ++ обычно генерируются как таблицы ветвей . Не так просто сгенерировать таблицу ветвей для переключения стиля строки.


11
Аргумент таблицы ветвления не должен применяться - это только один возможный подход, доступный автору компилятора. Для производственного компилятора приходится часто использовать несколько подходов в зависимости от сложности переключателя.
плинтус

5
@plinth, я поместил это там в основном по историческим причинам. Многие вопросы «почему C / C ++ делает это» легко могут быть найдены в истории компилятора. В то время, когда они написали это, C был прославлен сборкой и, следовательно, switch действительно был удобной таблицей ветвления.
JaredPar

114
Я отказываюсь, потому что не понимаю, как компилятор знает, как сравнивать 2 строковых значения в операторах if, но забывает способ сделать то же самое в операторах switch.

15
Я не думаю, что первые 2 абзаца являются вескими причинами. Особенно с C ++ 14, когда std::stringбыли добавлены литералы. Это в основном историческое. Но одна проблема, которая приходит на ум, заключается в том, что при том, как switchработает в настоящее время, дубликаты cases должны быть обнаружены во время компиляции; однако для строк это может быть не так просто (учитывая выбор локали во время выполнения и т. д.). Я предполагаю, что такая вещь должна была бы потребовать constexprслучаев или добавить неуказанное поведение (никогда, что мы хотим сделать).
ММ

8
Существует четкое определение того, как сравнивать два std::stringзначения или даже std::stringс массивом const char (а именно с помощью оператора ==), нет технической причины, которая помешала бы компилятору генерировать оператор switch для любого типа, который предоставляет этот оператор. Это могло бы открыть некоторые вопросы о таких вещах, как время жизни ярлыков, но в целом это в первую очередь решение по языку, а не техническая сложность.
MikeMB

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. Ваше решение выглядит чистым, но, к сожалению, все это противно. Решения на карте, представленные ниже, были бы лучше, и также избегали бы вызова функций. Кроме того, используя две карты, вы также можете встроить текст для регистрации ошибок.
Дирк Бестер

Вы также можете избежать перечисления с лямбдами: stackoverflow.com/a/42462552/895245
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功

Может ли 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
Вы должны убедиться, что ни в одном из ваших случаев хэш не имеет одинакового значения. И даже в этом случае могут возникнуть некоторые ошибки, когда другие строки, хэширующие, например, то же значение, что и хэш («один»), будут неправильно выполнять первое «что-то» в вашем переключателе.
Дэвид Льюнг Мэдисон Стеллар

Я знаю, но если он хеширует до одинакового значения, он не скомпилируется, и вы заметите это вовремя.
Ник

Хорошая мысль - но это не решает коллизию хешей для других строк, которые не являются частью вашего переключателя. В некоторых случаях это может не иметь значения, но если бы это было универсальное «переходное» решение, я мог бы представить, что в какой-то момент это проблема безопасности или тому подобное.
Дэвид Люнг Мэдисон Стеллар

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

Использует две карты для преобразования между строками и перечислением класса (лучше, чем обычное перечисление, потому что его значения находятся внутри него, и обратный поиск хороших сообщений об ошибках).

Использование static в коде codeguru возможно с поддержкой компилятором списков инициализаторов, что означает VS 2013 plus. 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), где вы можете хешировать строки case во время компиляции и хешировать строку переключателя во время выполнения. Возможно, это было бы целесообразно для действительно длинных коммутаторов, но, безусловно, даже менее обратно совместимых, если это имеет значение.
Дирк Бестер

Не могли бы вы поделиться решением для компиляции здесь, пожалуйста? Спасибо!
Qed

12

Проблема в том, что по причинам оптимизации оператор switch в C ++ не работает ни с чем, кроме примитивных типов, и вы можете только сравнить их с константами времени компиляции.

Предположительно, причина ограничения заключается в том, что компилятор может применить некоторую форму оптимизации, компилируя код до одной инструкции cmp и перехода, где адрес вычисляется на основе значения аргумента во время выполнения. Поскольку разветвление и циклы и плохо работают с современными процессорами, это может быть важной оптимизацией.

Чтобы обойти это, я боюсь, вам придется прибегнуть к заявлениям if.


Оптимизированная версия оператора switch, которая может работать со строками, определенно возможна. Тот факт, что они не могут повторно использовать один и тот же путь кода, который они используют для примитивных типов, не означает, что они не могут сделать std::stringдругих пользователей первыми в языке и поддержать их в выражении switch с помощью эффективного алгоритма.
ceztko

10

std::map + C ++ 11 лямбда-шаблон без перечислений

unordered_mapдля потенциальных амортизированных O(1): Каков наилучший способ использования HashMap в 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;
    }
}

Вывод:

one 1
two 2
three 3
foobar -1

Использование внутри методов с static

Чтобы эффективно использовать этот шаблон внутри классов, статически инициализируйте лямбда-карту, иначе вы платите O(n)каждый раз за ее создание с нуля.

Здесь мы можем избежать {}инициализации staticпеременной метода: статические переменные в методах класса , но мы также можем использовать методы, описанные в: статические конструкторы в C ++? Мне нужно инициализировать частные статические объекты

Необходимо было преобразовать захват лямбда-контекста [&]в аргумент, или это было бы неопределенным: 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выражении является ошибкой во время компиляции. Использование std::unordered_mapмолча принимает дубликаты значений.
Д.Шоули

6

В C ++ и C переключатели работают только с целочисленными типами. Вместо этого используйте лестницу if else. C ++, очевидно, мог бы реализовать какое-то выражение swich для строк - я думаю, никто не посчитал это полезным, и я согласен с ними.


согласился, но знаете ли вы, что сделало это невозможным для использования
yesraaj

История? Включение действительных чисел, указателей и структур (только для других типов данных в C) не имеет смысла, поэтому C ограничил его целыми числами.

Особенно, если вы включите классы, которые позволяют неявные преобразования, вы действительно хорошо проведете время один раз.
Sharptooth

6

Почему нет? Вы можете использовать реализацию переключателя с эквивалентным синтаксисом и той же семантикой. В Cязыке вообще нет объектов и строковых объектов, но строки в строках с Cнулевым символом в конце, на которые ссылается указатель. В 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)

Синтаксические различия с переключением языка

  • ключевые слова в верхнем регистре
  • нужны скобки для заявления CASE
  • точка с запятой ';' в конце заявления не допускается
  • двоеточие ':' в CASE не допускается
  • нужен один из ключевых слов BREAK или FALL в конце оператора CASE

Для C++97языка используется линейный поиск. Для C++11более современного можно использовать quickрежим поиска по дереву, где оператор возврата в CASE становится недопустимым. 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

Я думаю, что причина в том, что в C строки не являются примитивными типами, как сказал Томьен, мыслить в строке как массив символов, поэтому вы не можете делать такие вещи, как:

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

3
Без поиска, массив символов, скорее всего, выродится в символ *, который преобразуется непосредственно в целочисленный тип. Таким образом, он вполне может скомпилироваться, но, безусловно, не будет делать то, что вы хотите.
Дэвид Торнли

3

В с ++ строки не являются гражданами первого класса. Строковые операции выполняются через стандартную библиотеку. Я думаю, что это причина. Кроме того, C ++ использует оптимизацию таблицы ветвей для оптимизации операторов регистра переключателя. Посмотрите на ссылку.

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


2

В C ++ вы можете использовать только оператор switch для int и char


3
Символ также превращается в int.
Страгер

Указатели тоже могут. Это означает, что иногда вы можете скомпилировать что-то, что будет иметь смысл на другом языке, но это не будет работать правильно.
Дэвид Торнли

Вы можете на самом деле использовать longи long long, который не превратится в int. Там нет риска усечения там.
MSalters


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
Хотя этот код может ответить на вопрос, предоставление дополнительного контекста относительно того, почему и / или как этот код отвечает на вопрос, повышает его долгосрочную ценность.
Бенджамин В.

0

во многих случаях вы можете получить дополнительную работу, вытащив первый символ из строки и включив его. может случиться так, что вам придется выполнить вложенное переключение на charat (1), если ваши дела начинаются с того же значения. кто-нибудь, читающий ваш код, оценит подсказку


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. Вместо этого вы можете попробовать enum для представления строки и использовать ее в блоке переключателя, например:

enum MyString(raj,taj,aaj);

Используйте его в этом случае.



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

(код из Википедии https://en.wikipedia.org/wiki/Branch_table )


4
C ++ не требует какой-либо конкретной реализации своего синтаксиса. Наивная cmp/ jccреализация может быть такой же действительной в соответствии со стандартом C ++.
Руслан
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.