Perché l'istruzione switch non può essere applicata alle stringhe?


227

Compilando il codice seguente e ottenuto l'errore di type illegal.

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

Non è possibile utilizzare la stringa in switcho case. Perché? Esiste una soluzione che funzioni bene per supportare una logica simile all'accensione delle stringhe?


6
Esiste un'alternativa boost che nasconde la costruzione della mappa, enumata dietro un MACRO?
Balki

@balki Non sono sicuro di boost ma è facile scrivere macro di questo tipo. In caso di Qt , puoi nascondere la mappatura conQMetaEnum
phuclv il

Risposte:


189

Il motivo per cui ha a che fare con il sistema dei tipi. C / C ++ in realtà non supporta le stringhe come tipo. Supporta l'idea di un array di caratteri costante ma in realtà non comprende appieno la nozione di stringa.

Per generare il codice per un'istruzione switch, il compilatore deve capire cosa significa che due valori sono uguali. Per elementi come ints ed enum, questo è un confronto banale. Ma come dovrebbe il compilatore confrontare 2 valori di stringa? Distinzione tra maiuscole e minuscole, insensibile, sensibile alla cultura, ecc ... Senza una piena consapevolezza di una stringa, non è possibile rispondere in modo accurato.

Inoltre, le istruzioni switch C / C ++ vengono in genere generate come tabelle di diramazione . Generare una tabella di diramazione per un interruttore di tipo stringa non è altrettanto semplice.


11
L'argomento della tabella di diramazione non dovrebbe applicarsi - questo è solo un possibile approccio disponibile per un autore del compilatore. Per un compilatore di produzione, è necessario utilizzare frequentemente diversi approcci a seconda della complessità dello switch.
plinto

5
@plinth, l'ho messo lì principalmente per motivi storici. Molte delle domande "perché C / C ++ fa questo" possono essere facilmente risolte dalla cronologia del compilatore. Al momento in cui lo scrissero, C fu glorificato in assemblea e quindi switch era davvero una comoda tabella di derivazione.
JaredPar,

114
Voto in basso perché non capisco come potrebbe il compilatore sapere come confrontare 2 valori di stringa in istruzioni if ​​ma dimentico il modo di fare la stessa cosa in istruzioni switch.

15
Non credo che i primi 2 paragrafi siano validi motivi. Soprattutto dal C ++ 14 quando std::stringsono stati aggiunti valori letterali. È per lo più storico. Ma un problema che fa venire in mente è che, con il modo in cui switchfunziona attualmente, duplicare cases deve essere rilevato al momento della compilazione; tuttavia, ciò potrebbe non essere così semplice per le stringhe (considerando la selezione delle impostazioni internazionali di runtime e così via). Suppongo che una cosa del genere debba richiedere constexprcasi o aggiungere un comportamento non specificato (mai una cosa che vogliamo fare).
MM

8
Esiste una chiara definizione di come confrontare due std::stringvalori o anche uno std::stringcon un array di caratteri costanti (ovvero utilizzando l'operatore ==) non esiste alcun motivo tecnico che impedisca al compilatore di generare un'istruzione switch per qualsiasi tipo che fornisce tale operatore. Aprirà alcune domande su cose come la durata dei lables ma tutto sommato questa è principalmente una decisione di progettazione del linguaggio, non una difficoltà tecnica.
MikeMB,

60

Come accennato in precedenza, ai compilatori piace creare tabelle di ricerca che ottimizzino le switchistruzioni su tempi quasi O (1) quando possibile. Combina questo con il fatto che il linguaggio C ++ non ha un tipo di stringa - std::stringfa parte della libreria standard che non fa parte del linguaggio in sé.

Offrirò un'alternativa che potresti prendere in considerazione, l'ho usata in passato con buoni risultati. Invece di cambiare la stringa stessa, cambia il risultato di una funzione hash che usa la stringa come input. Il tuo codice sarà quasi chiaro come passare sopra la stringa se stai usando un set predeterminato di stringhe:

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

Ci sono un sacco di ovvie ottimizzazioni che praticamente seguono ciò che il compilatore C farebbe con un'istruzione switch ... divertente come ciò accada.


15
Questo è davvero deludente perché in realtà non stai hashing. Con il moderno C ++ puoi effettivamente eseguire l'hash in fase di compilazione utilizzando una funzione hash constexpr. La tua soluzione sembra pulita ma sfortunatamente ha tutta quella cattiva sorte. Le soluzioni cartografiche di seguito sarebbero migliori ed eviterebbero anche la chiamata di funzione. Inoltre, utilizzando due mappe è possibile avere un testo incorporato anche per la registrazione degli errori.
Dirk Bester,


L'hashit potrebbe essere una funzione constexpr? Dato che passi un carattere const * invece che uno std :: string.
Victor Stone,

Ma perché? Puoi sempre usare l'esecuzione dell'istruzione if sopra uno switch. Entrambi hanno un impatto minimo, ma i vantaggi in termini di prestazioni con un interruttore vengono cancellati dalla ricerca if-else. Il solo utilizzo di un if-else dovrebbe essere leggermente più veloce, ma soprattutto, significativamente più breve.
Zoe,

20

C ++

Funzione hash 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
Devi assicurarti che nessuno dei tuoi casi abbia lo stesso valore. E anche allora, potresti avere degli errori in cui altre stringhe che hanno l'hash, ad esempio, lo stesso valore dell'hash ("one") faranno erroneamente il primo "qualcosa" nel tuo switch.
David Ljung Madison Stellar,

Lo so, ma se ha lo stesso valore non si compila e lo noterai in tempo.
Nick,

Un buon punto, ma ciò non risolve la collisione dell'hash per altre stringhe che non fanno parte del tuo switch. In alcuni casi ciò potrebbe non avere importanza, ma se si trattasse di una soluzione "go-to" generica, a un certo punto potrei immaginare che si tratti di un problema di sicurezza o simili.
David Ljung Madison Stellar,

7
Puoi aggiungere a operator ""per rendere il codice più bello. constexpr inline unsigned int operator "" _(char const * p, size_t) { return hash(p); }E case "Peter"_: break;
usalo

15

Aggiornamento C ++ 11 apparentemente non sopra @MarmouCorp ma http://www.codeguru.com/cpp/cpp/cpp_mfc/article.php/c4067/Switch-on-Strings-in-C.htm

Utilizza due mappe per la conversione tra le stringhe e la classe enum (meglio della semplice enum perché i suoi valori sono contenuti al suo interno, e ricerca inversa per bei messaggi di errore).

L'uso di static nel codice codeguru è possibile con il supporto del compilatore per gli elenchi di inizializzatori, il che significa VS 2013 plus. gcc 4.8.1 era a posto con esso, non so quanto più indietro sarebbe compatibile.

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

Dovrei notare che in seguito ho trovato una soluzione che richiede valori letterali di stringa e calcoli del tempo di compilazione (penso C ++ 14 o 17) in cui è possibile eseguire l'hashing delle stringhe del case in fase di compilazione e eseguire l'hashing della stringa di switch in fase di runtime. Sarebbe utile per interruttori davvero lunghi forse, ma sicuramente anche meno compatibili con le versioni precedenti se ciò è importante.
Dirk Bester,

Potresti condividere la soluzione in fase di compilazione qui per favore? Grazie!
qed

12

Il problema è che, per motivi di ottimizzazione, l'istruzione switch in C ++ non funziona su nient'altro che sui tipi primitivi e puoi solo confrontarli con costanti di tempo di compilazione.

Presumibilmente il motivo della restrizione è che il compilatore è in grado di applicare una qualche forma di ottimizzazione compilando il codice fino a un'istruzione cmp e un goto in cui l'indirizzo viene calcolato in base al valore dell'argomento in fase di runtime. Poiché le ramificazioni e i loop non funzionano bene con le CPU moderne, questa può essere un'importante ottimizzazione.

Per ovviare a questo, temo che dovrai ricorrere alle dichiarazioni if.


Una versione ottimizzata di un'istruzione switch che può funzionare con le stringhe è sicuramente possibile. Il fatto che non possano riutilizzare lo stesso percorso di codice che usano per i tipi primitivi non significa che non possano fare std::stringe altri primi cittadini nella lingua e supportarli nell'istruzione switch con un algoritmo efficiente.
ceztko,

10

std::map + C ++ 11 modello lambdas senza enumerazioni

unordered_mapper il potenziale ammortizzato O(1): qual è il modo migliore per utilizzare una HashMap in 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;
    }
}

Produzione:

one 1
two 2
three 3
foobar -1

Utilizzo dei metodi interni con static

Per utilizzare questo modello in modo efficiente all'interno delle classi, inizializzare staticamente la mappa lambda, oppure si paga O(n)ogni volta per crearla da zero.

Qui possiamo {}evitare l' inizializzazione di una staticvariabile di metodo: variabili statiche nei metodi di classe , ma potremmo anche usare i metodi descritti in: costruttori statici in C ++? Devo inizializzare oggetti statici privati

Era necessario trasformare la cattura del contesto lambda [&]in un argomento, o che sarebbe stato indefinito: const static auto lambda utilizzato con la cattura per riferimento

Esempio che produce lo stesso output di cui sopra:

#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
Si noti che esiste una differenza tra questo approccio e switchun'istruzione. La duplicazione dei valori del caso in switchun'istruzione è un errore del tempo di compilazione. L'uso std::unordered_mapsilenzioso accetta valori duplicati.
D.Shawley,

6

In C ++ e C gli switch funzionano solo su tipi interi. Utilizzare invece una scala if else. Ovviamente il C ++ avrebbe potuto implementare una sorta di dichiarazione swich per le stringhe - immagino che nessuno abbia pensato che valesse la pena, e sono d'accordo con loro.


d'accordo, ma sai cosa ha reso impossibile l'uso di questo
yesraaj

Storia? L'attivazione di numeri reali, puntatori e strutture (solo altri tipi di dati di C) non fa sanse, quindi C lo limita a numeri interi.

Soprattutto se attivi le classi che consentono conversioni implicite, ti divertirai davvero una volta.
sharptooth,

6

Perchè no? È possibile utilizzare l' implementazione dello switch con sintassi equivalente e stessa semantica. Il Clinguaggio non ha affatto oggetti e oggetti stringhe, ma le stringhe Csono stringhe con terminazione null a cui fa riferimento il puntatore. Il C++linguaggio ha la possibilità di eseguire funzioni di sovraccarico per il confronto di oggetti o il controllo delle uguaglianze di oggetti. Come Ccome C++è sufficientemente flessibile per avere tale interruttore per le stringhe per la C lingua e per gli oggetti di qualsiasi tipo che il sostegno comparaison o l'uguaglianza di controllo per C++la lingua. E modernoC++11 consentono di implementare questo switch in modo abbastanza efficace.

Il tuo codice sarà così:

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"

È possibile utilizzare tipi più complicati, ad esempio, std::pairso qualsiasi struttura o classe che supporti operazioni di uguaglianza (o comarzioni per una rapida modalità ).

Caratteristiche

  • qualsiasi tipo di dati a supporto del confronto o del controllo dell'uguaglianza
  • possibilità di creare stati di stato switch nidificati in cascata.
  • possibilità di infrangere o ignorare le dichiarazioni dei casi
  • possibilità di usare espressioni non costanti
  • possibile abilitare la modalità statica / dinamica rapida con la ricerca dell'albero (per C ++ 11)

Le differenze Sintax con il cambio di lingua sono

  • parole chiave maiuscole
  • servono parentesi per la dichiarazione CASE
  • punto e virgola ';' alla fine delle dichiarazioni non è consentito
  • due punti ":" nell'istruzione CASE non sono consentiti
  • è necessaria una parola chiave BREAK o FALL alla fine dell'istruzione CASE

Per il C++97linguaggio utilizzato ricerca lineare. Per C++11e più moderno possibile utilizzare la quickmodalità di ricerca dell'albero wuth in modalità in cui l' istruzione return in CASE non è consentita. L' Cimplementazione del linguaggio esiste dovechar* vengono utilizzati confronti di tipo e stringhe con terminazione zero.

Ulteriori informazioni su questa implementazione di switch.


6

Per aggiungere una variazione usando il contenitore più semplice possibile (non c'è bisogno di una mappa ordinata) ... Non mi preoccuperei di un enum: basta mettere la definizione del contenitore immediatamente prima del passaggio in modo che sia facile vedere quale numero rappresenta quale caso.

Questo fa una ricerca con hash nel unordered_mape usa il associato intper guidare l'istruzione switch. Dovrebbe essere abbastanza veloce. Si noti che atviene utilizzato al posto di [], come ho creato quel contenitore const. L'uso []può essere pericoloso: se la stringa non è nella mappa, creerai una nuova mappatura e potresti finire con risultati indefiniti o una mappa in continua crescita.

Nota che la at()funzione genererà un'eccezione se la stringa non è nella mappa. Quindi potresti voler provare prima usando 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;


}

Segue la versione con un test per una stringa non definita:

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

Penso che il motivo sia che nelle stringhe C non ci sono tipi primitivi, come diceva tomjen, pensa in una stringa come un array di caratteri, quindi non puoi fare cose come:

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

3
Senza cercarlo, una matrice di caratteri degenererebbe probabilmente in un carattere *, che converte direttamente in un tipo integrale. Quindi, potrebbe benissimo essere compilato, ma sicuramente non farà ciò che vuoi.
David Thornley,

3

In c ++ le stringhe non sono cittadini di prima classe. Le operazioni sulle stringhe vengono eseguite tramite la libreria standard. Penso che questo sia il motivo. Inoltre, C ++ utilizza l'ottimizzazione della tabella delle filiali per ottimizzare le istruzioni del caso switch. Dai un'occhiata al link.

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


2

In C ++ puoi usare solo un'istruzione switch su int e char


3
Anche un carattere si trasforma in un int.
Strager

Anche i puntatori possono farlo. Ciò significa che a volte puoi compilare qualcosa che avrebbe senso in una lingua diversa, ma non funzionerà correttamente.
David Thornley,

Puoi effettivamente usare longe long long, che non si trasformerà in int. Non c'è rischio di troncamento lì.
Saluti


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
Mentre questo codice può rispondere alla domanda, fornendo ulteriore contesto riguardo al perché e / o al modo in cui questo codice risponde alla domanda migliora il suo valore a lungo termine.
Benjamin W.

0

in molti casi puoi evitare il lavoro extra estraendo il primo carattere dalla stringa e accendendolo. potrebbe finire con un passaggio nidificato su charat (1) se i tuoi casi iniziano con lo stesso valore. chiunque legga il tuo codice apprezzerebbe comunque un suggerimento perché la maggior parte verrebbe probabilmente solo if-else-if


0

Soluzione più funzionale al problema dello switch:

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

Gli switch funzionano solo con tipi integrali (int, char, bool, ecc.). Perché non usare una mappa per accoppiare una stringa con un numero e quindi usare quel numero con l'interruttore?


-2

Questo perché C ++ trasforma gli switch in jump table. Esegue un'operazione banale sui dati di input e passa all'indirizzo corretto senza confrontare. Poiché una stringa non è un numero, ma una matrice di numeri, C ++ non può creare una tabella di salto da essa.

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

(codice da wikipedia https://en.wikipedia.org/wiki/Branch_table )


4
Il C ++ non richiede un'implementazione particolare della sua sintassi. Un'ingenua cmp/ jccimplementazione può essere altrettanto valida secondo lo standard C ++.
Ruslan,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.