Чому оператор 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, але він не повністю розуміє поняття рядка.

Щоб генерувати код для оператора перемикача, компілятор повинен розуміти, що означає два рівні значення. Для таких предметів, як int та enums, це тривіальне бітове порівняння. Але як компілятор повинен порівнювати 2 рядкові значення? Враховуючи регістр, нечутливий, усвідомлюючи культуру тощо ... Без повної обізнаності про рядок це неможливо точно відповісти.

Додатково, оператори перемикань C / C ++, як правило, генеруються у вигляді галузевих таблиць . Генерувати таблицю гілок для перемикача стилю рядка не так просто.


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

5
@plinth, я розміщую це здебільшого з історичних причин. На багато питань "Чому C / C ++ це робить" можна легко відповісти історією компілятора. У той час, коли вони це писали, C прославляв збірку, а значить, перемикач справді був зручним столом для відділення.
JaredPar

114
Я голосую вниз, тому що я не розумію, як компілятор знає, як порівнювати 2 рядкові значення, якщо заяви, але забув спосіб зробити те ж саме в операторах переключення.

15
Я не думаю, що перші два абзаци є вагомими причинами. Тим більше що C ++ 14, коли std::stringбули додані літерали. Це здебільшого історичне. Але одна з проблем, яка виникає на увазі, полягає в тому, що із способом switchроботи, який зараз працює, дублюютьcase s повинні бути виявлені під час компіляції; однак це може бути не так просто для рядків (з урахуванням вибору локалів часу виконання тощо). Я гадаю, що така річ повинна вимагати constexprвипадків або додати невказану поведінку (ніколи того, що ми хочемо робити).
ММ

8
Існує чітке визначення того, як порівняти два std::stringзначення або навіть an std::stringз масивом const char (а саме за допомогою оператора ==), немає жодної технічної причини, яка заважала б компілятору генерувати оператор переключення для будь-якого типу, що забезпечує цей оператор. Це відкривало б деякі питання щодо таких речей, як термін служби лабірок, але все це в першу чергу рішення дизайну мови, а не технічні труднощі.
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 зробив би із заявою перемикача ... смішно як це відбувається.


15
Це насправді розчаровує, оскільки ви насправді не хешуєте. З сучасним C ++ ви можете фактично хешувати під час компіляції, використовуючи хеш-функцію constexpr. Ваше рішення виглядає чистим, але має все те неприємне, якщо сходи йдуть на жаль. Наведені нижче рішення на карті були б кращими, а також уникати виклику функцій. Крім того, використовуючи дві карти, ви також можете вбудувати текст для реєстрації помилок.
Дірк Бестер

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

Чи може хеш бути функцією 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; Demo
hare1039

15

Оновлення C ++ 11, мабуть, не @MarmouCorp вище, але http://www.codeguru.com/cpp/cpp/cpp_mfc/article.php/c4067/Switch-on-Strings-in-C.htm

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

Використання статики в коді codeguru можливо при підтримці компілятора списків ініціалізаторів, що означає VS 2013 плюс. 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, я думаю), де ви можете хешувати рядки справи під час компіляції та хеш-ланцюжок перемикання під час виконання. Можливо, варто мати дійсно довгі комутатори, можливо, але, звичайно, ще менш сумісні назад, якщо це має значення.
Дірк Бестер

Чи можете ви поділитися рішенням про час компіляції тут, будь ласка? Дякую!
ч.т.д.

12

Проблема полягає в тому, що з міркувань оптимізації оператор переключення в C ++ працює не на нічого, крім примітивних типів, і ви можете порівняти їх лише з константами часу компіляції.

Імовірно, причина обмеження полягає в тому, що компілятор може застосовувати певну форму оптимізації, компілюючи код до однієї інструкції cmp і goto, де адреса обчислюється на основі значення аргументу під час виконання. Оскільки розгалуження та циклі не грають добре із сучасними процесорами, це може бути важливою оптимізацією.

Щоб обійти це, я боюся, що вам доведеться вдаватися до заяв.


Оптимізована версія оператора switch, яка може працювати з рядками, безумовно можлива. Те, що вони не можуть використовувати той самий шлях коду, який вони використовують для примітивних типів, не означає, що вони не можуть зробити std::stringта інших громадян першими громадянами на мові та підтримувати їх у операторі комутатора за допомогою ефективного алгоритму.
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 статична автоматична лямбда, що використовується для зйомки за посиланням

Приклад, який дає такий же вихід, як і вище:

#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 if. C ++, очевидно, міг реалізувати якусь swich заяву для рядків - я думаю, ніхто не вважав це за варті, і я згоден з ними.


погодились, але чи знаєте ви, що зробило це неможливим для використання
yesraaj

Історія? Увімкнення реальних чисел, покажчиків та структур (єдині інші типи даних C) не дає сенсу, тому C обмежив це цілими числами.

Особливо, якщо ви перейдете на заняття, які дозволяють неявні перетворення, ви один раз будете дуже добре провести час.
гострий зуб

6

Чому ні? Ви можете використовувати реалізацію комутатора з еквівалентним синтаксисом і такою ж семантикою. У Cмові взагалі немає об'єктів і об'єктів рядків, але рядки в C- це нульові завершені рядки, на які посилається вказівник. C++Мови мають можливість здійснювати функції захисту від перевантаження для об'єктів Comparision або перевірок об'єктів рівності. В 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режим пошуку по дереву wuth, де оператор return в CASE стає забороненим. CРеалізація мови існує там , деchar* використовуються тип і закінчується нуль порівнянь.

Детальніше про реалізацію цього перемикача.


6

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

Це робить хешований пошук у 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}
};
// 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 не є примітивними типами, як сказав tomjen, мислити в рядку як масив char, тому не можна робити такі речі, як:

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

3
Не дивлячись на це, масив символів, ймовірно, переродився в знак char *, який перетворюється безпосередньо на цілісний тип. Отже, він цілком може скласти, але він точно не буде робити те, що ви хочете.
Девід Торнлі

3

У рядках c ++ не є громадянами першого класу. Операції з рядком виконуються через стандартну бібліотеку. Я думаю, це причина. Крім того, C ++ використовує оптимізацію таблиць гілок для оптимізації операторів вимикача. Перегляньте посилання.

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


2

У C ++ ви можете використовувати оператор переключення лише на int та char


3
Чар теж перетворюється на інту.
страгер

Покажчики також можуть. Це означає, що іноді ви можете скласти щось, що мало б сенс на іншій мові, але це не буде правильно.
Девід Торнлі

Ви насправді можете використовувати 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);

Використовуйте його у викладі справи swich.



-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 ++ не вимагає особливої ​​реалізації свого синтаксису. Наївність cmp/ jccреалізація може бути такою ж справедливою відповідно до Стандарту C ++.
Руслан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.