¿Por qué la declaración de cambio no se puede aplicar en cadenas?

227

Compilando el siguiente código y obtuve el error de type illegal.

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

No puede usar una cadena en switcho case. ¿Por qué? ¿Hay alguna solución que funcione bien para admitir una lógica similar a encender cadenas?

yesraaj
fuente
66
¿Existe una alternativa de impulso que oculte la construcción del mapa, enumeración detrás de un MACRO?
balki
@balki No estoy seguro acerca de boost pero es fácil escribir esas macros. En el caso de Qt , puede ocultar la asignación conQMetaEnum
phuclv

Respuestas:

189

El motivo tiene que ver con el sistema de tipos. C / C ++ realmente no admite cadenas como tipo. Es compatible con la idea de una matriz de caracteres constante, pero realmente no comprende completamente la noción de una cadena.

Para generar el código para una instrucción switch, el compilador debe comprender lo que significa que dos valores sean iguales. Para elementos como entradas y enumeraciones, esta es una comparación trivial de bits. Pero, ¿cómo debería el compilador comparar 2 valores de cadena? Sin distinción de mayúsculas y minúsculas, insensible, consciente de la cultura, etc. Sin un conocimiento completo de una cadena, esto no se puede responder con precisión.

Además, las instrucciones de cambio de C / C ++ generalmente se generan como tablas de ramificación . No es tan fácil generar una tabla de ramificación para un interruptor de estilo de cadena.

JaredPar
fuente
11
El argumento de la tabla de ramificación no debería aplicarse: ese es solo un enfoque posible disponible para un autor del compilador. Para un compilador de producción, uno tiene que usar con frecuencia varios enfoques dependiendo de la complejidad del conmutador.
zócalo
55
@plinth, lo puse allí principalmente por razones históricas. Muchas de las preguntas de "por qué C / C ++ hace esto" pueden ser respondidas fácilmente por el historial del compilador. En el momento en que lo escribieron, C era un ensamblaje glorificado y, por lo tanto, el interruptor realmente era una tabla de sucursal conveniente.
JaredPar
114
Voto hacia abajo porque no entiendo cómo podría el compilador saber cómo comparar 2 valores de cadena en las declaraciones if, pero olvido la forma de hacer lo mismo en las declaraciones switch.
15
No creo que los primeros 2 párrafos sean razones válidas. Especialmente desde C ++ 14 cuando std::stringse agregaron literales. Es sobre todo histórico. Sin embargo, un problema que viene a la mente es que con la forma switchtrabaja actualmente, duplicar cases debe ser detectada en tiempo de compilación; sin embargo, esto podría no ser tan fácil para las cadenas (teniendo en cuenta la selección de la configuración regional en tiempo de ejecución, etc.). Supongo que tal cosa debería requerir constexprcasos o agregar un comportamiento no especificado (nunca es algo que queramos hacer).
MM
8
Hay una definición clara de cómo comparar dos std::stringvalores o incluso uno std::stringcon una matriz de caracteres constantes (es decir, mediante el uso de operator ==) no hay ninguna razón técnica que impida que el compilador genere una declaración de cambio para cualquier tipo que proporcione ese operador. Abriría algunas preguntas sobre cosas como la vida útil de las etiquetas, pero en general, esto es principalmente una decisión de diseño del lenguaje, no una dificultad técnica.
MikeMB
60

Como se mencionó anteriormente, a los compiladores les gusta construir tablas de búsqueda que optimicen las switchdeclaraciones a un tiempo cercano a O (1) siempre que sea posible. Combine esto con el hecho de que el lenguaje C ++ no tiene un tipo de cadena, std::stringes parte de la Biblioteca estándar que no es parte del lenguaje per se.

Ofreceré una alternativa que quizás desee considerar, la he usado en el pasado con buenos resultados. En lugar de cambiar la cadena en sí, cambie el resultado de una función hash que usa la cadena como entrada. Su código será casi tan claro como cambiar la cadena si está utilizando un conjunto predeterminado de cadenas:

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

Hay un montón de optimizaciones obvias que siguen más o menos lo que el compilador de C haría con una declaración de cambio ... es curioso cómo sucede eso.

D.Shawley
fuente
15
Esto es realmente decepcionante porque en realidad no estás haciendo hashing. Con C ++ moderno, puede hacer hash en tiempo de compilación usando una función hash constexpr. Su solución se ve limpia pero tiene toda esa desagradable si desafortunadamente la escalera continúa. Las siguientes soluciones de mapas serían mejores y evitarían también la llamada a la función. Además, al usar dos mapas, también puede haber incorporado texto para el registro de errores.
Dirk Bester
También puede evitar la enumeración con lambdas: stackoverflow.com/a/42462552/895245
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
¿Podría ser hashit una función constexpr? Dado que pasa un const char * en lugar de un std :: string.
Victor Stone
¿Pero por qué? Obtiene todo el tiempo el uso de la ejecución de la instrucción if en la parte superior de un conmutador. Ambas tienen un impacto mínimo, pero las ventajas de rendimiento con un interruptor se borran con la búsqueda de if-else. Solo usar un if-else debería ser marginalmente más rápido, pero lo más importante, significativamente más corto.
Zoe
20

C ++

función 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
}
Mella
fuente
1
Debe asegurarse de que ninguno de sus casos tenga el mismo valor. E incluso entonces, es posible que tenga algunos errores en los que otras cadenas que hacen hash, por ejemplo, el mismo valor que hash ("uno") hagan incorrectamente el primer "algo" en su conmutador.
David Ljung Madison Stellar
Lo sé, pero si tiene el mismo valor, no se compilará y lo notará a tiempo.
Nick
Buen punto, pero eso no resuelve la colisión hash para otras cadenas que no son parte de su conmutador. En algunos casos, eso podría no importar, pero si se tratara de una solución genérica "ir a" podría imaginarme que es un problema de seguridad o similar en algún momento.
David Ljung Madison Stellar
77
Puede agregar un operator ""para hacer que el código sea más hermoso. constexpr inline unsigned int operator "" _(char const * p, size_t) { return hash(p); }Y case "Peter"_: break;
úsalo
15

Actualización de C ++ 11 de aparentemente no @MarmouCorp arriba pero http://www.codeguru.com/cpp/cpp/cpp_mfc/article.php/c4067/Switch-on-Strings-in-C.htm

Utiliza dos mapas para convertir entre las cadenas y la enumeración de la clase (mejor que la enumeración simple porque sus valores tienen un alcance dentro de ella, y la búsqueda inversa de mensajes de error agradables).

El uso de static en el código codeguru es posible con el soporte del compilador para las listas de inicializadores, lo que significa VS 2013 plus. gcc 4.8.1 estaba bien con él, no estoy seguro de cuánto más atrás sería compatible.

/// <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
fuente
Debo señalar que más tarde encontré una solución que requería literales de cadena y cálculos de tiempo de compilación (C ++ 14 o 17, creo) donde puede mezclar las cadenas de mayúsculas y minúsculas en el tiempo de compilación y la cadena del interruptor en tiempo de ejecución. Quizás valga la pena para interruptores realmente largos, pero ciertamente incluso menos compatibles con versiones anteriores si eso es importante.
Dirk Bester
¿Podría compartir la solución en tiempo de compilación aquí, por favor? ¡Gracias!
qed
12

El problema es que, por razones de optimización, la instrucción switch en C ++ no funciona en nada más que en tipos primitivos, y solo puede compararlos con constantes de tiempo de compilación.

Presumiblemente, la razón de la restricción es que el compilador puede aplicar alguna forma de optimización compilando el código en una instrucción cmp y un goto donde la dirección se calcula en función del valor del argumento en tiempo de ejecución. Dado que la ramificación y los bucles no funcionan bien con las CPU modernas, esta puede ser una optimización importante.

Para evitar esto, me temo que tendrá que recurrir a declaraciones if.

tomjen
fuente
Definitivamente es posible una versión optimizada de una instrucción switch que pueda funcionar con cadenas. El hecho de que no puedan reutilizar la misma ruta de código que usan para los tipos primitivos no significa que no puedan hacer std::stringy que otros sean los primeros ciudadanos en el idioma y los apoyen en la declaración de cambio con un algoritmo eficiente.
ceztko
10

std::map + C ++ 11 patrón lambdas sin enumeraciones

unordered_mappara el potencial amortizado O(1): ¿Cuál es la mejor manera de usar un HashMap en 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;
    }
}

Salida:

one 1
two 2
three 3
foobar -1

Uso dentro de métodos con static

Para usar este patrón de manera eficiente dentro de las clases, inicialice el mapa lambda estáticamente, o de lo contrario paga O(n)cada vez para construirlo desde cero.

Aquí podemos salir con la {}inicialización de una staticvariable de método: variables estáticas en métodos de clase , pero también podríamos usar los métodos descritos en: ¿ constructores estáticos en C ++? Necesito inicializar objetos estáticos privados

Era necesario transformar la captura de contexto lambda [&]en un argumento, o eso habría sido indefinido: const lambda automática estática utilizada con captura por referencia

Ejemplo que produce el mismo resultado que el anterior:

#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 郝海东 冠状 病 六四 事件 法轮功
fuente
3
Tenga en cuenta que hay una diferencia entre este enfoque y una switchdeclaración. La duplicación de valores de casos en una switchdeclaración es un error de tiempo de compilación. El uso std::unordered_mapsilencioso acepta valores duplicados.
D.Shawley
6

En C ++ y C, los conmutadores solo funcionan en tipos enteros. Use una escalera if else en su lugar. C ++ obviamente podría haber implementado algún tipo de declaración rápida para cadenas: supongo que nadie pensó que valía la pena, y estoy de acuerdo con ellas.


fuente
de acuerdo, pero ¿sabe lo que hizo que esto no sea posible utilizar
yesraaj
¿Historia? Activar números reales, punteros y estructuras (los únicos otros tipos de datos de C) no tiene sentido, por lo que C lo limitó a enteros.
Especialmente si activa las clases que permiten conversiones implícitas, lo pasará muy bien una vez.
diente afilado
6

Por qué no? Puede usar la implementación del interruptor con una sintaxis equivalente y la misma semántica. El Clenguaje no tiene objetos y objetos de cadenas en absoluto, pero las cadenas en Cson cadenas terminadas en nulo a las que hace referencia el puntero. El C++lenguaje tiene la posibilidad de realizar funciones de sobrecarga para comparar objetos o verificar la igualdad de los objetos. Como Cya C++es lo suficientemente flexible como para tener tal interruptor para las cadenas de C lenguaje y de objetos de cualquier tipo que comparaison apoyo o la igualdad de verificación de C++la lengua. Y los modernos C++11permiten que esta implementación del interruptor sea lo suficientemente efectiva.

Su código será así:

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"

Es posible utilizar tipos más complicados, por ejemplo, std::pairso cualquier estructura o clase que admita operaciones de igualdad (o comarizaciones para el modo rápido ).

Caracteristicas

  • Cualquier tipo de datos que apoyen comparaciones o verificación de igualdad
  • posibilidad de construir estados de conmutadores anidados en cascada.
  • posibilidad de romper o caer a través de declaraciones de casos
  • posibilidad de usar expresiones de caso no constantes
  • posible habilitar el modo rápido estático / dinámico con búsqueda de árbol (para C ++ 11)

Sintax diferencias con el cambio de idioma es

  • palabras clave mayúsculas
  • necesita paréntesis para la declaración CASE
  • punto y coma ';' al final de las declaraciones no está permitido
  • dos puntos ':' en la declaración CASE no está permitido
  • necesita una palabra clave BREAK o FALL al final de la declaración CASE

Para el C++97lenguaje se utiliza la búsqueda lineal. Para un modo C++11más moderno posible de utilizar la quickbúsqueda de árbol de wuth donde la declaración de devolución en CASE no se permite. La Cimplementación del lenguaje existe dondechar* se utilizan comparaciones de cadenas de tipo y terminadas en cero.

Lea más sobre la implementación de este interruptor.

oklas
fuente
6

Para agregar una variación usando el contenedor más simple posible (sin necesidad de un mapa ordenado) ... No me molestaría con una enumeración: solo coloque la definición del contenedor inmediatamente antes del interruptor para que sea fácil ver qué número representa cuyo caso.

Esto hace una búsqueda hash en el unordered_mapy usa el asociado intpara conducir la declaración de cambio. Debería ser bastante rápido. Tenga en cuenta que atse usa en lugar de [], ya que hice ese contenedor const. El uso []puede ser peligroso: si la cadena no está en el mapa, creará una nueva asignación y puede terminar con resultados indefinidos o un mapa en continuo crecimiento.

Tenga en cuenta que la at()función generará una excepción si la cadena no está en el mapa. Por lo tanto, es posible que desee probar primero 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;


}

La versión con una prueba para una cadena indefinida sigue:

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
fuente
4

Creo que la razón es que en C las cadenas no son tipos primitivos, como dijo tomjen, piense en una cadena como una matriz de caracteres, por lo que no puede hacer cosas como:

switch (char[]) { // ...
switch (int[]) { // ...
grilix
fuente
3
Sin buscarlo, una matriz de caracteres probablemente degeneraría en un char *, que se convierte directamente en un tipo integral. Por lo tanto, podría compilarse, pero ciertamente no hará lo que quiere.
David Thornley
3

En c ++ las cadenas no son ciudadanos de primera clase. Las operaciones de cadena se realizan a través de la biblioteca estándar. Creo que esa es la razón. Además, C ++ utiliza la optimización de la tabla de ramificación para optimizar las declaraciones de mayúsculas y minúsculas. Echa un vistazo al enlace.

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

chappar
fuente
2

En C ++ solo puede usar una instrucción switch en int y char

CodeMonkey1313
fuente
3
Un char también se convierte en int.
extraño
Los punteros también pueden. Eso significa que a veces puedes compilar algo que tendría sentido en un idioma diferente, pero no funcionará correctamente.
David Thornley
Realmente puedes usar longy long long, que no se convertirá en int. No hay riesgo de truncamiento allí.
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
fuente
44
Si bien este código puede responder la pregunta, proporcionar un contexto adicional con respecto a por qué y / o cómo responde la pregunta mejora su valor a largo plazo.
Benjamin W.
0

en muchos casos, puede obtener un trabajo adicional extrayendo el primer carácter de la cadena y activándolo. puede terminar teniendo que hacer un cambio anidado en charat (1) si sus casos comienzan con el mismo valor. Sin embargo, cualquiera que lea su código agradecería una pista porque la mayoría probablemente solo si-si-si

Marshall Taylor
fuente
0

Solución más funcional al problema del interruptor:

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
fuente
-1

No puede usar la cadena en la caja del interruptor. Solo se permiten int y char. En su lugar, puede probar enum para representar la cadena y usarla en el bloque de mayúsculas y minúsculas como

enum MyString(raj,taj,aaj);

Úselo en la declaración de caso swich.

añil
fuente
-1

Los interruptores solo funcionan con tipos integrales (int, char, bool, etc.). ¿Por qué no usar un mapa para emparejar una cadena con un número y luego usar ese número con el interruptor?

derpface
fuente
-2

Eso es porque C ++ convierte los interruptores en tablas de salto. Realiza una operación trivial en los datos de entrada y salta a la dirección correcta sin comparar. Como una cadena no es un número, sino una matriz de números, C ++ no puede crear una tabla de salto a partir de ella.

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 de wikipedia https://en.wikipedia.org/wiki/Branch_table )

Jean-Luc Nacif Coelho
fuente
44
C ++ no requiere ninguna implementación particular de su sintaxis. Una ingenua cmp/ jccimplementación puede ser tan válida de acuerdo con el Estándar C ++.
Ruslan