Dlaczego instrukcja switch nie może być stosowana na ciągach?

227

Kompilowanie następującego kodu i wystąpił błąd type illegal .

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

Nie można użyć łańcucha w żadnym z switchlub case. Czemu? Czy jest jakieś rozwiązanie, które działa ładnie, obsługując logikę podobną do przełączania ciągów?

tak
źródło
6
Czy istnieje alternatywa wzmocnienia, która ukrywa budowę mapy, wyliczenie za MAKRO?
balki
@balki Nie jestem pewien co do ulepszenia, ale łatwo jest pisać takie makra. W przypadku Qt możesz ukryć mapowanie za pomocąQMetaEnum
phuclv

Odpowiedzi:

189

Powód, dla którego ma to związek z systemem typów. C / C ++ tak naprawdę nie obsługuje ciągów jako typu. Obsługuje ideę stałej tablicy znaków, ale tak naprawdę nie w pełni rozumie pojęcie łańcucha.

Aby wygenerować kod instrukcji switch, kompilator musi zrozumieć, co to znaczy, że dwie wartości są równe. W przypadku elementów takich jak int i wyliczenia jest to trywialne porównanie bitów. Ale w jaki sposób kompilator powinien porównać 2 wartości ciągu? Rozróżniana jest wielkość liter, nieczułość, świadomość kulturowa itp. Bez pełnej świadomości łańcucha nie można dokładnie odpowiedzieć.

Ponadto instrukcje przełączania C / C ++ są zazwyczaj generowane jako tabele gałęzi . Wygenerowanie tabeli rozgałęzień dla przełącznika stylu łańcucha nie jest tak łatwe.

JaredPar
źródło
11
Argument tabeli rozgałęzień nie powinien mieć zastosowania - to tylko jedno możliwe podejście dostępne dla autora kompilatora. W przypadku kompilatora produkcyjnego trzeba często stosować kilka podejść w zależności od złożoności przełącznika.
cokół
5
@plinth, umieszczam go tam głównie ze względów historycznych. Na wiele pytań „dlaczego C / C ++ to robi” można łatwo odpowiedzieć w historii kompilatora. W czasie, gdy to pisali, C był uwielbionym zbiorem, a zatem przełącznik był naprawdę wygodnym stołem gałęzi.
JaredPar
114
Głosuję w dół, ponieważ nie rozumiem, w jaki sposób kompilator może wiedzieć, jak porównać 2 wartości ciągu w instrukcjach if, ale zapomnieć o tym, jak zrobić to samo w instrukcjach switch.
15
Nie sądzę, aby pierwsze 2 akapity były ważnymi powodami. Zwłaszcza od C ++ 14, kiedy std::stringdodano literały. Jest to głównie historia. Ale jednym z problemów, który przychodzi mi na myśl, jest to, że przy switchobecnym sposobie działania duplikaty cases muszą zostać wykryte w czasie kompilacji; może to jednak nie być takie łatwe dla łańcuchów (biorąc pod uwagę wybór ustawień regionalnych w czasie wykonywania itd.). Podejrzewam, że taka rzecz musiałaby wymagać constexprprzypadków lub dodać nieokreślone zachowanie (nigdy nie jest to rzecz, którą chcemy robić).
MM
8
Istnieje jasna definicja sposobu porównywania dwóch std::stringwartości, a nawet std::stringz tablicą const char (mianowicie za pomocą operatora ==), nie ma technicznego powodu, który uniemożliwiłby kompilatorowi wygenerowanie instrukcji przełączania dla dowolnego typu udostępniającego tego operatora. Otworzyłoby to kilka pytań na temat życia lables, ale w sumie jest to przede wszystkim decyzja dotycząca projektu języka, a nie trudność techniczna.
MikeMB
60

Jak wspomniano wcześniej, kompilatory lubią budować tabele wyszukiwania, które optymalizują switchinstrukcje tak, aby zbliżały się do czasu O (1), gdy tylko jest to możliwe. Połącz to z faktem, że język C ++ nie ma typu łańcucha - std::stringjest częścią biblioteki standardowej, która nie jest częścią języka jako takiego.

Oferuję alternatywę, którą możesz rozważyć, korzystałem z niej w przeszłości z dobrym skutkiem. Zamiast przełączania samego łańcucha, przełącz wynik funkcji skrótu, która używa łańcucha jako danych wejściowych. Twój kod będzie prawie tak wyraźny, jak przełączanie ciągu, jeśli używasz określonego zestawu ciągów:

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

Istnieje kilka oczywistych optymalizacji, które podążają za tym, co zrobiłby kompilator C z instrukcją switch ... zabawne, jak to się dzieje.

D.Shawley
źródło
15
To jest naprawdę rozczarowujące, ponieważ tak naprawdę nie masz nic przeciwko. Dzięki nowoczesnemu C ++ możesz faktycznie mieszać w czasie kompilacji za pomocą funkcji skrótu constexpr. Twoje rozwiązanie wygląda czysto, ale niestety wszystko jest okropne. Poniższe rozwiązania mapowe byłyby lepsze i unikałyby również wywołania funkcji. Dodatkowo, używając dwóch map, możesz również wbudować tekst do rejestrowania błędów.
Dirk Bester
Możesz także uniknąć wyliczenia za pomocą lambdas: stackoverflow.com/a/42462552/895245
Ciro Santilli 法轮功 冠状 病 六四 事件 法轮功
Czy hashit może być funkcją constexpr? Biorąc pod uwagę, że przekazujesz const char * zamiast std :: string.
Victor Stone
Ale dlaczego? Cały czas korzystasz z wykonania instrukcji if na przełączniku. Oba mają minimalny wpływ, ale zalety wydajności przełącznika są usuwane przez wyszukiwanie if-else. Samo użycie if-else powinno być nieznacznie szybsze, ale co ważniejsze, znacznie krótsze.
Zoe
20

C ++

funkcja skrótu 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
}
Nacięcie
źródło
1
Musisz upewnić się, że żaden z twoich przypadków nie ma takiej samej wartości. I nawet wtedy możesz mieć błędy, gdy inne ciągi, które mają skrót, na przykład tej samej wartości, co skrót („jeden”), niepoprawnie zrobią pierwsze „coś” na twoim przełączniku.
David Ljung Madison Stellar
Wiem, ale jeśli ma taką samą wartość, nie skompiluje się, a zauważysz to na czas.
Nick
Dobra uwaga - ale to nie rozwiązuje kolizji skrótu dla innych ciągów, które nie są częścią przełącznika. W niektórych przypadkach może to nie mieć znaczenia, ale jeśli byłoby to ogólne rozwiązanie typu „przejdź do”, mógłbym w pewnym momencie wyobrazić sobie, że jest to kwestia bezpieczeństwa.
David Ljung Madison Stellar
7
Możesz dodać a, operator ""aby kod był piękniejszy. constexpr inline unsigned int operator "" _(char const * p, size_t) { return hash(p); }I używaj go jakcase "Peter"_: break; Demo
hare1039
15

Aktualizacja C ++ 11 najwyraźniej nie @MarmouCorp powyżej, ale http://www.codeguru.com/cpp/cpp/cpp_mfc/article.php/c4067/Switch-on-Strings-in-C.htm

Używa dwóch map do konwersji między ciągami znaków i wyliczeniem klasy (lepiej niż zwykły wyliczenie, ponieważ jego wartości są w nim zakreślone, i odwrotne wyszukiwanie w celu uzyskania miłych komunikatów o błędach).

Zastosowanie kodu statycznego w kodzie codeguru jest możliwe dzięki obsłudze kompilatora dla list inicjalizacyjnych, co oznacza VS 2013 plus. gcc 4.8.1 było w porządku, nie jestem pewien, o ile dalej będzie kompatybilny.

/// <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]);
}
Dirk Bester
źródło
Powinienem zauważyć, że później znalazłem rozwiązanie wymagające literałów łańcuchowych i obliczeń czasu kompilacji (myślę, że C ++ 14 lub 17), w którym można mieszać ciągi znaków w czasie kompilacji i mieszać ciąg przełączania w czasie wykonywania. Byłoby warto na naprawdę długie przełączniki, ale na pewno jeszcze mniej kompatybilne wstecz, jeśli to ma znaczenie.
Dirk Bester,
Czy mógłbyś podzielić się tutaj rozwiązaniem czasu kompilacji? Dzięki!
qed
12

Problem polega na tym, że ze względu na optymalizację instrukcja switch w C ++ nie działa tylko na typach pierwotnych i można je porównywać tylko ze stałymi czasowymi kompilacji.

Przypuszczalnie powodem tego ograniczenia jest to, że kompilator jest w stanie zastosować jakąś formę optymalizacji kompilując kod do jednej instrukcji cmp i goto, gdzie adres jest obliczany na podstawie wartości argumentu w czasie wykonywania. Ponieważ rozgałęzienia i pętle nie działają dobrze z nowoczesnymi procesorami, może to być ważna optymalizacja.

Aby obejść ten problem, obawiam się, że będziesz musiał uciekać się do oświadczeń.

tomjen
źródło
Zoptymalizowana wersja instrukcji switch, która może pracować z łańcuchami, jest zdecydowanie możliwa. Fakt, że nie mogą ponownie użyć tej samej ścieżki kodu, której używają dla typów pierwotnych, nie oznacza, że ​​nie mogą zrobić, std::stringa inni pierwsi obywatele w języku i wspierać ich w instrukcji switch za pomocą wydajnego algorytmu.
ceztko
10

std::map + C ++ 11 wzór lambda bez wyliczeń

unordered_mapdla potencjalnie zamortyzowanego O(1): Jaki jest najlepszy sposób użycia HashMap w 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;
    }
}

Wynik:

one 1
two 2
three 3
foobar -1

Użycie w metodach z static

Aby efektywnie wykorzystać ten wzorzec wewnątrz klas, zainicjuj mapę lambda statycznie, albo płacisz za O(n)każdym razem, aby zbudować ją od zera.

Tutaj możemy uciec od {}inicjalizacji staticzmiennej metody: Zmienne statyczne w metodach klasowych , ale moglibyśmy również użyć metod opisanych w: statyczne konstruktory w C ++? Muszę zainicjować prywatne obiekty statyczne

Konieczne było przekształcenie przechwytywania kontekstu lambda [&]w argument, który byłby niezdefiniowany: const static auto lambda używany z przechwytywaniem przez odniesienie

Przykład, który daje takie same wyniki jak powyżej:

#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;
    }
}
Ciro Santilli
źródło
3
Zauważ, że istnieje różnica między tym podejściem a switchstwierdzeniem. Powielanie wartości wielkości liter w switchinstrukcji jest błędem czasowym kompilacji. Używanie w trybie std::unordered_mapcichym przyjmuje zduplikowane wartości.
D.Shawley
6

W C ++ i C przełączniki działają tylko na typach całkowitych. Zamiast tego użyj drabiny if else. C ++ mógł oczywiście zaimplementować jakąś instrukcję swich dla łańcuchów - chyba nikt nie uważał, że warto, i zgadzam się z nimi.


źródło
Zgadzam się
Historia? Włączenie liczb rzeczywistych, wskaźników i struktur (tylko inne typy danych C) nie powoduje sanse, więc C ograniczył to do liczb całkowitych.
Zwłaszcza jeśli włączysz klasy, które umożliwiają niejawne konwersje, będziesz się naprawdę dobrze bawić.
sharptooth
6

Dlaczego nie? Można użyć implementacji przełącznika z równoważną składnią i taką samą semantyką. CJęzyk nie posiada obiektów i smyczki obiekty w ogóle, ale w struny Cjest null zakończone ciągi odwołują wskaźnika. C++Język mają możliwość dokonania funkcje przeciążenie dla obiektów porównania lub sprawdzanie obiektów równości. Tak Cjak C++jest na tyle elastyczny, aby mieć taki przełącznik do ciągów dla C języka i dla każdego typu obiektów, które comparaison wsparcie lub równość czek na C++języku. Nowoczesne C++11pozwalają na wystarczająco efektywne wdrożenie tego przełącznika.

Twój kod będzie taki:

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"

Możliwe jest użycie bardziej skomplikowanych typów, na przykład, std::pairsdowolnych struktur lub klas, które obsługują operacje równości (lub komendy dla szybkiego trybu ).

cechy

  • każdy rodzaj danych, które wspierają porównania lub sprawdzanie równości
  • możliwość budowania kaskadowych statystyk przełączników zagnieżdżonych.
  • możliwość złamania lub przewrócenia się instrukcji
  • możliwość użycia wyrażeń wielkości liter, które nie są spójne
  • możliwe włączenie szybkiego trybu statycznego / dynamicznego z wyszukiwaniem drzewa (dla C ++ 11)

Różnice Sintax z przełączaniem języków są

  • wielkie słowa kluczowe
  • potrzebujesz nawiasów do instrukcji CASE
  • średnik ”;” na końcu instrukcji jest niedozwolone
  • dwukropek „:” w instrukcji CASE jest niedozwolony
  • potrzebujesz jednego ze słów kluczowych BREAK lub FALL na końcu instrukcji CASE

Do C++97wyszukiwania liniowego używanego języka. Do C++11bardziej nowoczesnego możliwego do użycia quicktrybu wyszukiwania drzewa wuth, w którym instrukcja return w CASE staje się niedozwolona. CRealizacja język gdzie istniejechar* używany jest typ i zerowej zakończony Porównania smyczkowych.

Przeczytaj więcej o tej implementacji przełącznika.

oklas
źródło
6

Aby dodać odmianę przy użyciu najprostszego możliwego pojemnika (nie ma potrzeby zamówienia uporządkowanej mapy) ... nie zawracałbym sobie głowy wyliczeniem - wystarczy umieścić definicję kontenera bezpośrednio przed przełącznikiem, aby łatwo było zobaczyć, która liczba reprezentuje która sprawa

Spowoduje to wyszukiwanie skrótowe unordered_mapi użycie powiązanego intdo sterowania instrukcją switch. Powinno być dość szybkie. Zauważ, że atjest używany zamiast [], ponieważ zrobiłem ten pojemnik const. Używanie []może być niebezpieczne - jeśli łańcucha nie ma na mapie, utworzysz nowe mapowanie i może to skutkować nieokreślonymi wynikami lub stale rosnącą mapą.

Zauważ, że at()funkcja zgłosi wyjątek, jeśli ciągu nie ma na mapie. Więc możesz najpierw przetestować za pomocą 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;


}

Wersja z testem na niezdefiniowany ciąg znaków wygląda następująco:

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

}
rsjaffe
źródło
4

Myślę, że powodem jest to, że w C łańcuchy nie są prymitywnymi typami, jak powiedział tomjen, myśl w łańcuchu jako tablica char, więc nie możesz robić takich rzeczy:

switch (char[]) { // ...
switch (int[]) { // ...
grilix
źródło
3
Bez wyszukiwania tablica znaków prawdopodobnie zdegenerowałaby się do znaku char *, który przekształca się bezpośrednio w typ integralny. Więc może się dobrze skompilować, ale na pewno nie zrobi tego, co chcesz.
David Thornley,
3

W c ++ łańcuchy nie są obywatelami pierwszej klasy. Operacje na łańcuchach są wykonywane za pomocą standardowej biblioteki. Myślę, że to jest powód. Ponadto C ++ korzysta z optymalizacji tabeli rozgałęzień w celu optymalizacji instrukcji case switch. Spójrz na link.

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

chappar
źródło
2

W C ++ można używać tylko instrukcji switch na int i char

CodeMonkey1313
źródło
3
Char zmienia się również w int.
strager
Wskaźniki też mogą. Oznacza to, że czasami możesz skompilować coś, co miałoby sens w innym języku, ale nie będzie działać poprawnie.
David Thornley,
Możesz użyć longi long long, co się nie zmieni int. Nie ma tam ryzyka obcięcia.
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;

    }
Juan Llanes
źródło
4
Chociaż ten kod może odpowiedzieć na pytanie, zapewnienie dodatkowego kontekstu dotyczącego tego, dlaczego i / lub jak ten kod odpowiada na pytanie, poprawia jego długoterminową wartość.
Benjamin W.
0

w wielu przypadkach można uniknąć dodatkowej pracy, wyciągając pierwszy znak z łańcucha i włączając go. może skończyć się koniecznością wykonania zagnieżdżonego przełącznika na charat (1), jeśli twoje przypadki zaczynają się od tej samej wartości. każdy czytający Twój kod z pewnością doceniłby podpowiedź, ponieważ większość z nich spróbowałaby tylko jeśli-inaczej-jeśli

Marshall Taylor
źródło
0

Bardziej funkcjonalne obejście problemu z przełącznikiem:

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)
    {
        // ...
    }
}
FelikZ
źródło
-1

Nie można używać ciągu znaków w przypadku przełącznika. Dozwolone są tylko int i char. Zamiast tego możesz wypróbować wyliczenie do reprezentowania ciągu i użyć go w bloku skrzynki przełączników, takim jak

enum MyString(raj,taj,aaj);

Użyj go w instrukcji case swich.

indygowiec
źródło
-1

Przełączniki działają tylko z typami integralnymi (int, char, bool itp.). Dlaczego nie użyć mapy do sparowania ciągu z liczbą, a następnie użyć tego numeru z przełącznikiem?

derpface
źródło
-2

To dlatego, że C ++ zamienia przełączniki w tabele skoków. Wykonuje trywialną operację na danych wejściowych i przeskakuje pod właściwy adres bez porównywania. Ponieważ ciąg nie jest liczbą, ale tablicą liczb, C ++ nie może z niej utworzyć tabeli skoków.

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

(kod z wikipedii https://en.wikipedia.org/wiki/Branch_table )

Jean-Luc Nacif Coelho
źródło
4
C ++ nie wymaga żadnej konkretnej implementacji jego składni. Naiwna cmp/ jccimplementacja może być równie ważna zgodnie ze standardem C ++.
Ruslan