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?


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.


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.


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


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
}

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

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


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

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.



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.


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

}

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

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


2

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


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;

    }

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


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

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



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


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


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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.