¿Por qué no se puede aplicar la instrucción switch a las cadenas?

13 minutos de lectura

avatar de usuario
yesraaj

Compilar el siguiente código da el mensaje de error: type illegal.

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

No puedes usar cadenas en ninguno de los dos switch o case. ¿Por qué? ¿Hay alguna solución que funcione bien para admitir una lógica similar a la de activar cadenas?

  • ¿Existe una alternativa de impulso que oculte la construcción del mapa, enum detrás de una MACRO?

    – balki

    2 de marzo de 2012 a las 15:07

  • @balki No estoy seguro sobre el impulso, pero es fácil escribir este tipo de macros. En el caso de Qt, puede ocultar el mapeo con QMetaEnum

    – phuclv

    19 de enero de 2018 a las 8:44

La razón por la cual tiene que ver con el sistema de tipos. C/C++ realmente no admite cadenas como tipo. Admite la idea de una matriz de caracteres constante, pero en realidad no comprende completamente la noción de una cadena.

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

Además, las declaraciones de cambio de C/C++ generalmente se generan como mesas de ramas. No es tan fácil generar una tabla de ramas para un interruptor de estilo de cadena.

  • El argumento de la tabla de ramas 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 cambio.

    – pedestal

    16 de marzo de 2009 a las 13:32

  • Voto en contra porque no entiendo cómo podría el compilador saber cómo comparar 2 valores de cadena en declaraciones if pero olvidar la forma de hacer lo mismo en declaraciones de cambio.

    usuario955249

    12 de junio de 2012 a las 8:45

  • No creo que los primeros 2 párrafos sean razones válidas. Especialmente desde C++14 cuando std::string Se agregaron literales. Es sobre todo histórico. Pero un problema que me viene a la mente es que con la forma switch funciona actualmente, duplicado cases deber ser detectado en tiempo de compilación; sin embargo, esto podría no ser tan fácil para las cadenas (considerando la selección de la configuración regional en tiempo de ejecución, etc.). Supongo que tal cosa tendría que requerir constexpr casos, o agregar un comportamiento no especificado (nunca algo que queramos hacer).

    –MM

    11 de junio de 2015 a las 23:37


  • Hay una definición clara de cómo comparar dos std::string valores o incluso un std::string con una matriz de caracteres const (es decir, usando 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.

    – Mike MB

    15 de noviembre de 2017 a las 15:51

  • La razón por la que el compilador no sabe cómo comparar una cadena es pobre. Una mejor razón es la reticencia del comité estándar de c ++ para hacer std::string, std::wstring y otros tipos de cadena primer ciudadano en el idioma.

    – ceztko

    3 oct 2018 a las 22:18

Como se mencionó anteriormente, a los compiladores les gusta construir tablas de búsqueda que optimicen switch sentencias con sincronización cercana a O(1) siempre que sea posible. Combine esto con el hecho de que el lenguaje C++ no tiene un tipo de cadena: std::string es parte de la Biblioteca Estándar que no es parte del Idioma 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 haría el compilador de C con una declaración de cambio… es curioso cómo sucede eso.

  • Esto es realmente decepcionante porque en realidad no estás haciendo hash. Con C ++ moderno, puede hacer hash en tiempo de compilación usando una función hash constexpr. Su solución se ve limpia, pero desafortunadamente tiene toda esa desagradable escalera. Las soluciones de mapa a continuación serían mejores y también evitarían la llamada de función. Además, al usar dos mapas, también puede tener texto incorporado para el registro de errores.

    – Dirk Bester

    20 mayo 2016 a las 18:18

  • También puede evitar la enumeración con lambdas: stackoverflow.com/a/42462552/895245

    – Ciro Santilli OurBigBook.com

    17 de marzo de 2018 a las 9:16

  • ¿Podría hashit ser una función constexpr? Dado que pasa un const char * en lugar de un std::string.

    – Víctor Piedra

    27 de marzo de 2018 a las 17:19

  • ¿Pero por qué? Obtiene todo el tiempo el uso de la ejecución de la declaración if en la parte superior de un interruptor. Ambos tienen un impacto mínimo, pero las ventajas de rendimiento con un interruptor se borran con la búsqueda if-else. Solo usar un if-else debería ser marginalmente más rápido, pero lo que es más importante, significativamente más corto.

    – Zoe apoya a Ucrania

    27 mayo 2020 a las 19:36

  • Estoy de acuerdo con @Zoe, ¿cuál es el punto de todo este código adicional cuando un simple if/else será suficiente? Si me encuentro con este ejemplo en el campo, lo eliminaría y lo refactorizaría para que sea un if else, que es más legible y sostenible para los equipos. Incluso la versión más pequeña y simple de esto que usa una función hash constexpr es fea e ilegible.

    – Sonny Parlin

    28 de enero a las 22:08

avatar de usuario
Mella

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
}

Actualizar:

El ejemplo anterior es C++11. Ahí constexpr la función debe ser con una sola declaración. Esto se relajó en las siguientes versiones de C++.

En C ++ 14 y C ++ 17 puede usar la siguiente función hash:

constexpr uint32_t hash(const char* data, size_t const size) noexcept{
    uint32_t hash = 5381;

    for(const char *c = data; c < data + size; ++c)
        hash = ((hash << 5) + hash) + (unsigned char) *c;

    return hash;
}

También C++ 17 tiene std::string_viewpor lo que puede usarlo en lugar de const char *.

En C++ 20, puedes intentar usar consteval.

  • Debe asegurarse de que ninguno de sus casos tenga el mismo valor. E incluso entonces, puede tener algunos errores en los que otras cadenas que tienen el mismo valor que hash (“uno”), por ejemplo, harán incorrectamente el primer “algo” en su interruptor.

    – David Ljung Madison Estelar

    27/04/2018 a las 21:54

  • Lo sé, pero si tiene el mismo valor, no se compilará y lo notará a tiempo.

    – Nick

    29 de abril de 2018 a las 6:52

  • Buen punto, pero eso no resuelve la colisión de hash para otras cadenas que no forman parte de su interruptor. En algunos casos, eso podría no importar, pero si se tratara de una solución genérica “ir a”, podría imaginar que se trata de un problema de seguridad o similar en algún momento.

    – David Ljung Madison Estelar

    30 de abril de 2018 a las 22:49


  • Puedes agregar un operator "" para hacer el código más hermoso. constexpr inline unsigned int operator "" _(char const * p, size_t) { return hash(p); } Y úsalo como case "Peter"_: break; Manifestación

    – liebre1039

    13 de febrero de 2019 a las 17:32


  • Esto es realmente genial para las declaraciones de cambio, ¡gracias por publicar esta solución!

    – Ray Hulha

    29 de marzo de 2021 a las 15:10

avatar de usuario
dirk bester

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 clase (mejor que la enumeración simple porque sus valores están dentro del alcance y la búsqueda inversa para mensajes de error agradables).

El uso de estática 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 eso, 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]);
}

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

Presumiblemente, el motivo 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 las bifurcaciones 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.

  • Definitivamente es posible una versión optimizada de una declaración de cambio 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::string y otros primeros ciudadanos en el idioma y apoyarlos en la declaración de cambio con un algoritmo eficiente.

    – ceztko

    06/10/2018 a las 13:01

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

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

Producción:

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 de forma estática, o de lo contrario paga O(n) cada vez para construirlo desde cero.

Aquí podemos salirnos con la {} inicialización de un static variable 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 static auto lambda used with capture by reference

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

  • Definitivamente es posible una versión optimizada de una declaración de cambio 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::string y otros primeros ciudadanos en el idioma y apoyarlos en la declaración de cambio con un algoritmo eficiente.

    – ceztko

    06/10/2018 a las 13:01

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_map y utiliza el asociado int para impulsar la sentencia switch. Debería ser bastante rápido. Tenga en cuenta que at se utiliza en lugar de []como he hecho ese contenedor const. Usando [] puede ser peligroso: si la cadena no está en el mapa, creará un nuevo mapeo y puede terminar con resultados indefinidos o un mapa en continuo crecimiento.

Tenga en cuenta que el at() La función lanzará 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

}

¿Ha sido útil esta solución?