Почему оператор switch нельзя применять к строкам?
Компилируя следующий код и получил ошибку type illegal
,
int main()
{
// Compilation error - switch expression of type illegal
switch(std::string("raj"))
{
case"sda":
}
}
Вы не можете использовать строку либо switch
или же case
, Зачем? Есть ли какое-нибудь решение, которое хорошо работает для поддержки логики, похожей на включение строк?
24 ответа
Причина, почему это связано с системой типов. C/C++ не поддерживает строки как тип. Он поддерживает идею постоянного массива символов, но не совсем понимает понятие строки.
Чтобы сгенерировать код для оператора switch, компилятор должен понимать, что означает, что два значения равны. Для таких элементов, как int и enums, это тривиальное сравнение битов. Но как компилятор должен сравнивать 2 строковых значения? Чувствителен к регистру, нечувствителен, учитывает культуру и т. Д. Без полного понимания строки невозможно ответить точно.
Кроме того, операторы переключения C / C++ обычно генерируются как таблицы ветвей. Не так просто сгенерировать таблицу ветвей для переключения стиля строки.
Как упоминалось ранее, компиляторы любят создавать таблицы поиска, которые оптимизируют 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... забавно, как это происходит.
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
}
Вы можете использовать только переключение примитивов, таких как int, char и enum. Самое простое решение сделать это, как вы хотите, это использовать 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;
}
Код, написанный Стефаном Раком 25 июля 2001 года.
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]);
}
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;
}
}
Проблема в том, что по причинам оптимизации оператор switch в C++ не работает ни с чем, кроме примитивных типов, и вы можете только сравнить их с константами времени компиляции.
Предположительно, причина ограничения заключается в том, что компилятор может применить некоторую форму оптимизации, компилируя код до одной инструкции cmp и перехода, где адрес вычисляется на основе значения аргумента во время выполнения. Поскольку разветвление и циклы и плохо работают с современными процессорами, это может быть важной оптимизацией.
Чтобы обойти это, я боюсь, вам придется прибегнуть к заявлениям if.
Чтобы добавить вариант с использованием простейшего возможного контейнера (нет необходимости в упорядоченной карте)... Я не стал бы беспокоиться с перечислением - просто поместите определение контейнера непосредственно перед переключателем, чтобы было легко увидеть, какое число представляет в каком случае.
Это делает хешированный поиск в unordered_map
и использует связанный int
управлять заявлением переключателя. Должно быть довольно быстро. Обратите внимание, что 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}
};
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
}
В C++ и C переключатели работают только с целочисленными типами. Вместо этого используйте лестницу if else. C++, очевидно, мог бы реализовать какое-то выражение swich для строк - я думаю, никто не посчитал это полезным, и я согласен с ними.
Почему бы и нет? Вы можете использовать реализацию переключателя с эквивалентным синтаксисом и той же семантикой. C
язык вообще не имеет объектов и строковых объектов, но C
это строки с нулевым символом в конце, на которые ссылается указатель. C++
язык имеет возможность создавать функции перегрузки для сравнения объектов или проверки равенства объектов. Как C
как C++
достаточно гибок, чтобы иметь такой переключатель для строк для C
язык и для объектов любого типа, которые поддерживают сравнение или проверку равенства для 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*
используется сравнение строк с типом и нулем в конце.
Узнайте больше об этой реализации переключателя.
Я думаю, что причина в том, что в C строки не являются примитивными типами, как сказал Томьен, мыслить в строке как массив символов, поэтому вы не можете делать такие вещи, как:
switch (char[]) { // ...
switch (int[]) { // ...
В с ++ строки не являются гражданами первого класса. Строковые операции выполняются через стандартную библиотеку. Я думаю, что это причина. Кроме того, C++ использует оптимизацию таблицы ветвления для оптимизации операторов регистра переключателя. Посмотрите на ссылку.
Поздно к вечеринке, вот решение, которое я придумал некоторое время назад, которое полностью соответствует запрошенному синтаксису.
#include <uberswitch/uberswitch.hpp>
int main()
{
uberswitch (std::string("raj"))
{
case ("sda"): /* ... */ break; //notice the parenthesis around the value.
}
}
заяц комментарий к решению Ника действительно крутой. вот полный пример кода (на C++11):
constexpr uint32_t hash(const std::string& s) noexcept
{
uint32_t hash = 5381;
for (const auto& c : s)
hash = ((hash << 5) + hash) + (unsigned char)c;
return hash;
}
constexpr inline uint32_t operator"" _(char const* p, size_t) { return hash(p); }
std::string s = "raj";
switch (hash(s)) {
case "sda"_:
// do_something();
break;
default:
break;
}
В C++ вы можете использовать только оператор switch для int и char
Вот элегантный способ включить «строки» времени компиляции (на самом деле string_view) с нулевыми накладными расходами во время выполнения. Можно использовать массив строк constexpr, а затем включить функцию consteval, как показано ниже. Вывод компилятора идентичен использованию только целых чисел — дополнительная память или производительность во время выполнения не оплачиваются. Код компиляции доступен здесь https://godbolt.org/z/KqjKrczv5 .
#include <fmt/core.h>
#include <array>
#include <string_view>
static constexpr auto MY_MODES = std::array< std::string_view, 3 >{ "AAA", "BBB", "CCC" };
//use consteval to eliminate runtime conversions, zero runtime overhead!
consteval int mode( std::string_view s )
{
for( int i = 0; i < MY_MODES.size(); ++i )
if( std::string_view{ s } == MY_MODES[i] )
return i;
}
int main()
{
auto curMode = mode("CCC"); //use the "string_view"
fmt::print( "curMode is {:d}\n", curMode );
switch( curMode )
{
case mode( "AAA" ): fmt::print( "aaa" ); break;
case mode( "BBB" ): fmt::print( "bbb" ); break;
case mode( "CCC" ): fmt::print( "ccc" ); break;
default:
return -1;
}
return 42;
}
Используя этот подход, сборка оптимизируется как типичный оператор переключения — никаких дополнительных инструкций ЦП, поскольку все оценивается во время компиляции.
Вы можете поместить строки в массив и использовать для преобразования их в индексы во время компиляции.
constexpr const char* arr[] = { "bar", "foo" };
constexpr int index(const char* str) { /*...*/ }
do_something(std::string str)
{
switch(quick_index(str))
{
case index("bar"):
// ...
break;
case index("foo"):
// ...
break;
case -1:
default:
// ...
break;
}
Для
quick_index
, чего не должно быть, вы можете, например, использовать
unordered_map
сделать это O (1) во время выполнения.(Или отсортируйте массив и используйте двоичный поиск, см. Здесь пример .)
Вот полный пример для C++ 11 с простым настраиваемым компаратором строк. Повторяющиеся случаи и случаи не в массиве (
index
дает
-1
) будет обнаружен во время компиляции. Пропавших без вести случаев явно не обнаружено. Более поздние версии C++ имеют более гибкие
constexpr
выражения, позволяющие упростить код.
#include <iostream>
#include <algorithm>
#include <unordered_map>
constexpr const char* arr[] = { "bar", "foo", "foobar" };
constexpr int cmp(const char* str1, const char* str2)
{
return *str1 == *str2 && (!*str1 || cmp(str1+1, str2+1));
}
constexpr int index(const char* str, int pos=0)
{
return pos == sizeof(arr)/sizeof(arr[0]) ? -1 : cmp(str, arr[pos]) ? pos : index(str,pos+1);
}
int main()
{
// initialize hash table once
std::unordered_map<std::string,int> lookup;
int i = 0;
for(auto s : arr) lookup[s] = i++;
auto quick_index = [&](std::string& s)
{ auto it = lookup.find(s); return it == lookup.end() ? -1 : it->second; };
// usage in code
std::string str = "bar";
switch(quick_index(str))
{
case index("bar"):
std::cout << "bartender" << std::endl;
break;
case index("foo"):
std::cout << "fighter" << std::endl;
break;
case index("foobar"):
std::cout << "fighter bartender" << std::endl;
break;
case -1:
default:
std::cout << "moo" << std::endl;
break;
}
}
Вы можете использовать переключатель на струнах. Что вам нужно, это таблица строк, проверьте каждую строку
char** strings[4] = {"Banana", "Watermelon", "Apple", "Orange"};
unsigned get_case_string(char* str, char** _strings, unsigned n)
{
while(n)
{
n--
if(strcmp(str, _strings[n]) == 0) return n;
}
return 0;
}
unsigned index = get_case_string("Banana", strings, 4);
switch(index)
{
case 1: break;/*Found string `Banana`*/
default: /*No string*/
}
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;
}
Более функциональный обход проблемы переключения:
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)
{
// ...
}
}
Вы не можете использовать строку в случае переключения. Допускаются только int и char. Вместо этого вы можете попробовать enum для представления строки и использовать ее в блоке переключателя, например:
enum MyString(raj,taj,aaj);
Используйте его в заявлении.
Переключатели работают только с целочисленными типами (int, char, bool и т. Д.). Почему бы не использовать карту для сопряжения строки с номером, а затем использовать этот номер с переключателем?
Это потому, что 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)
Во многих случаях вы можете получить дополнительную работу, вытащив первый символ из строки и включив его. может случиться так, что вам придется выполнить вложенное переключение на charat(1), если ваши дела начинаются с того же значения. кто-нибудь, читающий ваш код, оценит подсказку