Por que a instrução switch não pode ser aplicada em strings?


227

Compilando o código a seguir e obtendo o erro de type illegal.

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

Você não pode usar string em switchou case. Por quê? Existe alguma solução que funcione bem para oferecer suporte a lógica semelhante à ativação de strings?


6
Existe uma alternativa de impulso que oculta a construção do mapa, atrás de um MACRO?
balki

@balki Não tenho certeza do impulso, mas é fácil escrever essas macros. No caso do Qt, você pode ocultar o mapeamento comQMetaEnum
phuclv

Respostas:


189

A razão pela qual tem a ver com o sistema de tipos. C / C ++ não suporta realmente cadeias de caracteres como um tipo. Ele suporta a idéia de uma matriz de caracteres constante, mas na verdade não entende completamente a noção de uma string.

Para gerar o código para uma instrução switch, o compilador deve entender o que significa dois valores iguais. Para itens como ints e enums, essa é uma comparação pouco trivial. Mas como o compilador deve comparar 2 valores de string? Diferencia maiúsculas de minúsculas, insensibilidade, cultura, etc ... Sem uma percepção completa de uma string, isso não pode ser respondido com precisão.

Além disso, as instruções de opção C / C ++ geralmente são geradas como tabelas de ramificação . Não é tão fácil gerar uma tabela de ramificação para uma opção de estilo de string.


11
O argumento da tabela de ramificação não deve se aplicar - essa é apenas uma abordagem possível disponível para um autor do compilador. Para um compilador de produção, é necessário usar frequentemente várias abordagens, dependendo da complexidade do comutador.
plinth

5
@ plinth, eu coloquei lá principalmente por razões históricas. Muitas das perguntas "por que o C / C ++ faz isso" podem ser facilmente respondidas pelo histórico do compilador. No momento em que o escreveram, C era uma montagem glorificada e, portanto, a troca era realmente uma tabela de ramificação conveniente.
JaredPar

114
Eu voto para baixo porque eu não entendo como o compilador sabe como comparar 2 valores de string em instruções if, mas esqueça a maneira de fazer a mesma coisa nas instruções switch.

15
Não acho que os dois primeiros parágrafos sejam razões válidas. Especialmente desde C ++ 14, quando std::stringliterais foram adicionados. É principalmente histórico. Mas um problema que vem à mente é que, com a maneira como switchfunciona atualmente, duplicados cases devem ser detectados em tempo de compilação; no entanto, isso pode não ser tão fácil para seqüências de caracteres (considerando a seleção de localidade em tempo de execução e assim por diante). Suponho que algo assim deva exigir constexprcasos ou adicionar um comportamento não especificado (nunca algo que queremos fazer).
MM

8
Há uma definição clara de como comparar dois std::stringvalores ou mesmo uma std::stringmatriz com char const (ou seja, usando o operador ==); não há razão técnica que impeça o compilador de gerar uma instrução switch para qualquer tipo que forneça esse operador. Isso abriria algumas questões sobre coisas como a vida útil dos rótulos, mas tudo isso é principalmente uma decisão de design de linguagem, não uma dificuldade técnica.
MikeMB

60

Como mencionado anteriormente, os compiladores gostam de criar tabelas de pesquisa que otimizam switchinstruções para o tempo próximo de O (1) sempre que possível. Combine isso com o fato de que a linguagem C ++ não possui um tipo de string - std::stringfaz parte da Biblioteca Padrão, que não faz parte da linguagem em si.

Vou oferecer uma alternativa que você pode querer considerar, eu a usei no passado com bons resultados. Em vez de alternar sobre a própria string, alterne o resultado de uma função hash que usa a string como entrada. Seu código será quase tão claro quanto alternar a string se você estiver usando um conjunto predeterminado de strings:

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

Existem várias otimizações óbvias que seguem o que o compilador C faria com uma instrução switch ... engraçado como isso acontece.


15
Isso é realmente decepcionante, porque você não está realmente usando hash. Com o C ++ moderno, é possível fazer o hash em tempo de compilação usando uma função de hash constexpr. Sua solução parece limpa, mas tem toda essa desagradabilidade se a escada continuar infelizmente. As soluções de mapas abaixo seriam melhores e também evitariam a chamada de função. Além disso, usando dois mapas, você também pode criar texto para registro de erros.
Dirk Bester

Você também pode evitar a enumeração com lambdas: stackoverflow.com/a/42462552/895245
Ciro Santilli

O hashit poderia ser uma função constexpr? Dado que você passa um const char * em vez de um std :: string.
Victor Stone

Mas por que? Você usa o tempo todo a execução da instrução if na parte superior de um comutador. Ambos têm um impacto mínimo, mas as vantagens de desempenho de um switch são apagadas pela pesquisa if-else. O uso de um if-else deve ser marginalmente mais rápido, mas mais importante, significativamente menor.
Zoe

20

C ++

função 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
Você precisa garantir que nenhum dos seus casos tenha o mesmo valor. E mesmo assim, você pode ter alguns erros em que outras seqüências de caracteres com hash, por exemplo, o mesmo valor que o hash ("one") incorretamente fazem o primeiro "algo" no seu comutador.
David Ljung Madison Stellar

Eu sei, mas se ele tiver o mesmo valor, ele não será compilado e você perceberá a tempo.
29418 Nick

Bom ponto - mas isso não resolve a colisão de hash para outras strings que não fazem parte do seu switch. Em alguns casos, isso pode não ter importância, mas se essa for uma solução genérica para "ir para", posso imaginar que isso seja um problema de segurança ou algo semelhante em algum momento.
David Ljung Madison Stellar

7
Você pode adicionar a operator ""para tornar o código mais bonito. constexpr inline unsigned int operator "" _(char const * p, size_t) { return hash(p); }E usá-lo como case "Peter"_: break; demonstração
hare1039

15

Atualização do C ++ 11 aparentemente não @MarmouCorp acima, mas http://www.codeguru.com/cpp/cpp/cpp_mfc/article.php/c4067/Switch-on-Strings-in-C.htm

Usa dois mapas para converter entre as seqüências de caracteres e a enumeração de classe (melhor que a enumeração simples, porque seus valores têm escopo dentro dela e pesquisa inversa para obter boas mensagens de erro).

O uso de static no código do codeguru é possível com o suporte do compilador para listas de inicializadores, o que significa o VS 2013 plus. O gcc 4.8.1 estava bem com isso, não sei quanto tempo atrás seria compatível.

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

Devo observar que, mais tarde, encontrei uma solução que requer literais de seqüência de caracteres e cálculos de tempo de compilação (C ++ 14 ou 17, eu acho) em que você pode fazer o hash das seqüências de caracteres de caso em tempo de compilação e a sequência de alternar em tempo de execução. Talvez valha a pena para switches realmente longos, mas talvez ainda menos compatíveis com versões anteriores, se isso importa.
quer

Você poderia compartilhar a solução em tempo de compilação aqui, por favor? Obrigado!
qed

12

O problema é que, por razões de otimização, a instrução switch no C ++ não funciona em nada além de tipos primitivos, e você só pode compará-los com constantes de tempo de compilação.

Presumivelmente, o motivo da restrição é que o compilador é capaz de aplicar alguma forma de otimização compilando o código em uma instrução cmp e em um local em que o endereço é calculado com base no valor do argumento em tempo de execução. Como ramificações e loops não funcionam bem com CPUs modernas, isso pode ser uma otimização importante.

Para contornar isso, receio que você precise recorrer a declarações if.


Uma versão otimizada de uma instrução switch que pode trabalhar com strings é definitivamente possível. O fato de eles não poderem reutilizar o mesmo caminho de código usado para tipos primitivos não significa que eles não podem criar std::stringe outros cidadãos em primeiro lugar no idioma e apoiá-los na instrução switch com um algoritmo eficiente.
Ceztko # 6/18

10

std::map + C ++ 11 padrão lambdas sem enums

unordered_mappara o potencial amortizado O(1): Qual é a melhor maneira de usar um HashMap em 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;
    }
}

Resultado:

one 1
two 2
three 3
foobar -1

Uso dentro de métodos com static

Para usar esse padrão de maneira eficiente dentro das classes, inicialize o mapa lambda estaticamente, ou então você paga O(n)sempre para construí-lo do zero.

Aqui, podemos nos safar da {}inicialização de uma staticvariável de método: Variáveis estáticas em métodos de classe , mas também poderíamos usar os métodos descritos em: construtores estáticos em C ++? Eu preciso inicializar objetos estáticos privados

Era necessário transformar a captura de contexto lambda [&]em um argumento, ou isso teria sido indefinido: const static auto lambda usado com captura por referência

Exemplo que produz a mesma saída que acima:

#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
Observe que há uma diferença entre essa abordagem e uma switchdeclaração. A duplicação de valores de caso em uma switchinstrução é uma falha no tempo de compilação. O uso std::unordered_mapsilencioso aceita valores duplicados.
precisa saber é o seguinte

6

Em C ++ e C, os switches funcionam apenas em tipos inteiros. Use uma escada if else. Obviamente, o C ++ poderia ter implementado algum tipo de declaração swich para strings - acho que ninguém achou que valesse a pena e concordo com eles.


concordou, mas você sabe o que fez este não é possível utilizar
yesraaj

História? Ativar números reais, ponteiros e estruturas (apenas outros tipos de dados C) não faz sanse, então C o limitou a números inteiros.

Especialmente se você ativar as classes que permitem conversões implícitas, você se divertirá muito uma vez.
Sharptooth # 16/9

6

Por que não? Você pode usar a implementação do switch com sintaxe equivalente e a mesma semântica. O Cidioma não possui objetos e objetos de cadeias, mas cadeias de caracteres Csão cadeias terminadas nulas referenciadas pelo ponteiro. A C++linguagem tem a possibilidade de criar funções de sobrecarga para comparação de objetos ou verificação da igualdade de objetos. Como Ccomo C++é suficiente flexível para ter essa mudança para cordas para C a linguagem e para objetos de qualquer tipo que o apoio comparaison ou igualdade cheque de C++linguagem. E os modernos C++11permitem que essa implementação de switch seja eficaz o suficiente.

Seu código será assim:

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"

É possível usar tipos mais complicados, por exemplo, std::pairsou quaisquer estruturas ou classes que suportem operações de igualdade (ou comarcas para o modo rápido ).

Recursos

  • qualquer tipo de dado que suporte comparações ou verificação de igualdade
  • possibilidade de construir statemens de switch aninhados em cascata.
  • possibilidade de quebrar ou cair através de declarações de caso
  • possibilidade de usar expressões de caso não constantes
  • possível ativar o modo estático / dinâmico rápido com a pesquisa em árvore (para C ++ 11)

As diferenças Sintax com a mudança de idioma são

  • palavras-chave maiúsculas
  • precisa de parênteses para a instrução CASE
  • ponto e vírgula ';' no final das declarações não é permitido
  • dois pontos ':' na instrução CASE não é permitido
  • precisa de uma das palavras-chave BREAK ou FALL no final da instrução CASE

Para o C++97idioma usado pesquisa linear. É C++11possível, de maneira mais moderna, usar a quickpesquisa em árvore wuth em modo, onde a instrução de retorno no CASE não é permitida. A Cimplementação do idioma existe onde as char*comparações de tipo e seqüência de caracteres terminadas em zero são usadas.

Leia mais sobre esta implementação de switch.


6

Para adicionar uma variação usando o contêiner mais simples possível (não há necessidade de um mapa ordenado) ... Eu não me incomodaria com uma enumeração - basta colocar a definição do contêiner imediatamente antes do comutador, para que seja fácil ver qual número representa qual caso.

Isso faz uma pesquisa de hash no unordered_mape usa o associado intpara conduzir a instrução switch. Deve ser bem rápido. Observe que até usado em vez de [], como eu fiz esse contêiner const. O uso []pode ser perigoso - se a sequência não estiver no mapa, você criará um novo mapeamento e poderá resultar em resultados indefinidos ou em um mapa em crescimento contínuo.

Observe que a at()função lançará uma exceção se a string não estiver no mapa. Então, você pode querer testar primeiro 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;


}

A versão com um teste para uma sequência indefinida é a seguinte:

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

Eu acho que a razão é que, em C, as strings não são tipos primitivos, como Tomjen disse, pensam em uma string como uma matriz de caracteres, então você não pode fazer coisas como:

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

3
Sem procurar, uma matriz de caracteres provavelmente se degenerará em um caractere *, que converte diretamente em um tipo integral. Portanto, pode muito bem compilar, mas certamente não fará o que você deseja.
David Thornley

3

Em c ++, as strings não são cidadãos de primeira classe. As operações de string são feitas através da biblioteca padrão. Eu acho que é essa a razão. Além disso, o C ++ usa a otimização da tabela de ramificação para otimizar as instruções de maiúsculas e minúsculas. Dê uma olhada no link.

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


2

Em C ++, você pode usar apenas uma instrução switch em int e char


3
Um char também se transforma em um int.
Strager 16/03/09

Os ponteiros também podem. Isso significa que às vezes você pode compilar algo que faria sentido em um idioma diferente, mas não funcionará corretamente.
21730 David Thornley

Você pode realmente usar longe long long, o que não se tornará int. Não há risco de truncamento lá.
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
Embora esse código possa responder à pergunta, fornecer um contexto adicional a respeito de por que e / ou como esse código responde à pergunta melhora seu valor a longo prazo.
Benjamin W.

0

Em muitos casos, você pode evitar trabalho extra, puxando o primeiro caractere da string e ativando-o. pode acabar precisando fazer uma troca aninhada no charat (1) se seus casos começarem com o mesmo valor. qualquer pessoa que esteja lendo seu código apreciaria uma dica, porque a maioria provavelmente tentaria apenas se-mais-se


0

Solução alternativa mais funcional para o problema do comutador:

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

Os comutadores funcionam apenas com tipos integrais (int, char, bool etc.). Por que não usar um mapa para emparelhar uma string com um número e depois usá-lo com a opção?


-2

Isso ocorre porque o C ++ transforma os switches em tabelas de salto. Ele executa uma operação trivial nos dados de entrada e salta para o endereço correto sem comparar. Como uma string não é um número, mas uma matriz de números, o C ++ não pode criar uma tabela de salto a partir dela.

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

(código da wikipedia https://en.wikipedia.org/wiki/Branch_table )


4
O C ++ não requer nenhuma implementação específica de sua sintaxe. Uma ingênua cmp/ jccimplementação pode ser igualmente válida de acordo com o Padrão C ++.
Ruslan
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.