Pourquoi l'instruction switch ne peut pas être appliquée aux chaînes?

227

Compilation du code suivant et erreur de type illegal .

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

Vous ne pouvez pas utiliser la chaîne dans les deux switchou case. Pourquoi? Existe-t-il une solution qui fonctionne bien pour prendre en charge une logique similaire à l'activation des chaînes?

yesraaj
la source
6
Existe-t-il une alternative de boost qui cache la construction de la carte, énumérée derrière une MACRO?
balki
@balki Je ne suis pas sûr de boost mais il est facile d'écrire de telles macros. Dans le cas de Qt, vous pouvez masquer le mappage avecQMetaEnum
phuclv

Réponses:

189

La raison en est liée au système de type. C / C ++ ne prend pas vraiment en charge les chaînes en tant que type. Il prend en charge l'idée d'un tableau de caractères constants mais il ne comprend pas vraiment la notion de chaîne.

Pour générer le code d'une instruction switch, le compilateur doit comprendre ce que cela signifie pour deux valeurs égales. Pour des éléments comme les entiers et les énumérations, il s'agit d'une comparaison de bits triviale. Mais comment le compilateur doit-il comparer 2 valeurs de chaîne? Sensible à la casse, insensible, sensible à la culture, etc.

En outre, les instructions de commutateur C / C ++ sont généralement générées sous forme de tables de branche . Il n'est pas aussi facile de générer une table de branche pour un commutateur de style de chaîne.

JaredPar
la source
11
L'argument de la table de branchement ne devrait pas s'appliquer - ce n'est qu'une approche possible pour un auteur de compilateur. Pour un compilateur de production, il faut fréquemment utiliser plusieurs approches en fonction de la complexité du commutateur.
socle
5
@plinth, je l'ai mis principalement pour des raisons historiques. Un grand nombre des questions «pourquoi C / C ++ fait-il cela» peuvent facilement être résolues par l'historique du compilateur. Au moment où ils l'ont écrit, C était un assemblage glorifié et donc le commutateur était vraiment une table de branchement pratique.
JaredPar
114
Je vote parce que je ne comprends pas comment le compilateur sait comment comparer 2 valeurs de chaîne dans les instructions if, mais oublie la façon de faire la même chose dans les instructions switch.
15
Je ne pense pas que les 2 premiers paragraphes soient des raisons valables. Surtout depuis C ++ 14 lorsque des std::stringlittéraux ont été ajoutés. C'est surtout historique. Mais un problème qui ne vient à l' esprit est que , avec la façon dont switchfonctionne actuellement, en double cases doit être détectée lors de la compilation; cependant, cela peut ne pas être aussi facile pour les chaînes (compte tenu de la sélection des paramètres régionaux au moment de l'exécution, etc.). Je suppose qu'une telle chose devrait nécessiter des constexprcas, ou ajouter un comportement non spécifié (jamais quelque chose que nous voulons faire).
MM
8
Il existe une définition claire de la façon de comparer deux std::stringvaleurs ou même un std::stringavec un tableau de caractères const (à savoir en utilisant l'opérateur ==), il n'y a aucune raison technique qui empêcherait le compilateur de générer une instruction switch pour tout type qui fournit cet opérateur. Cela ouvrirait quelques questions sur des choses comme la durée de vie des étiquettes, mais dans l'ensemble, c'est principalement une décision de conception de la langue, pas une difficulté technique.
MikeMB
60

Comme mentionné précédemment, les compilateurs aiment construire des tables de recherche qui optimisent les switchinstructions pour se rapprocher de O (1) dans la mesure du possible. Combinez cela avec le fait que le langage C ++ n'a pas de type chaîne - std::stringfait partie de la bibliothèque standard qui ne fait pas partie du langage en soi.

Je vais vous proposer une alternative que vous voudrez peut-être envisager, je l'ai utilisée dans le passé à bon escient. Au lieu de basculer sur la chaîne elle-même, basculez sur le résultat d'une fonction de hachage qui utilise la chaîne en entrée. Votre code sera presque aussi clair que de basculer sur la chaîne si vous utilisez un ensemble de chaînes prédéterminé:

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

Il y a un tas d'optimisations évidentes qui suivent à peu près ce que le compilateur C ferait avec une instruction switch ... drôle comment cela se passe.

D.Shawley
la source
15
C'est vraiment décevant car vous ne hachez pas réellement. Avec le C ++ moderne, vous pouvez réellement hacher au moment de la compilation en utilisant une fonction de hachage constexpr. Votre solution a l'air propre mais a tout ce méchant si l'échelle passe malheureusement. Les solutions de carte ci-dessous seraient meilleures et éviteraient également l'appel de fonction. De plus, en utilisant deux cartes, vous pouvez également avoir intégré du texte pour la journalisation des erreurs.
Dirk Bester
Vous pouvez également éviter l'énumération avec les lambdas: stackoverflow.com/a/42462552/895245
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
Le hachage peut-il être une fonction constexpr? Étant donné que vous passez un const char * plutôt qu'une chaîne std ::.
Victor Stone
Mais pourquoi? Vous obtenez tout le temps l'utilisation de l'exécution de l'instruction if au-dessus d'un commutateur. Les deux ont un impact minimal, mais les avantages de performances avec un commutateur sont effacés par la recherche if-else. L'utilisation d'un if-else devrait être légèrement plus rapide, mais surtout, beaucoup plus courte.
Zoé
20

C ++

fonction de hachage 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
}
pseudo
la source
1
Vous devez vous assurer qu'aucun de vos cas n'ait la même valeur. Et même alors, vous pouvez avoir des erreurs où d'autres chaînes qui hachent, par exemple, la même valeur que le hachage ("un") feront incorrectement le premier "quelque chose" dans votre commutateur.
David Ljung Madison Stellar
Je sais, mais s'il a la même valeur, il ne se compilera pas et vous le remarquerez à temps.
Nick
Bon point - mais cela ne résout pas la collision de hachage pour les autres chaînes qui ne font pas partie de votre commutateur. Dans certains cas, cela n'a peut-être pas d'importance, mais s'il s'agissait d'une solution générique "go-to", je pourrais imaginer qu'il s'agit d'un problème de sécurité ou similaire à un moment donné.
David Ljung Madison Stellar
7
Vous pouvez ajouter un operator ""pour rendre le code plus beau. constexpr inline unsigned int operator "" _(char const * p, size_t) { return hash(p); }Et utilisez-le comme case "Peter"_: break; Démo
hare1039
15

Mise à jour C ++ 11 apparemment pas @MarmouCorp ci-dessus mais http://www.codeguru.com/cpp/cpp/cpp_mfc/article.php/c4067/Switch-on-Strings-in-C.htm

Utilise deux mappes pour convertir entre les chaînes et l'énumération de classe (mieux que l'énumération simple parce que ses valeurs sont limitées à l'intérieur, et recherche inversée pour de beaux messages d'erreur).

L'utilisation de statique dans le code codeguru est possible avec le support du compilateur pour les listes d'initialisation, ce qui signifie VS 2013 plus. gcc 4.8.1 était d'accord, je ne sais pas combien de temps il serait 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
la source
Je dois noter que j'ai trouvé plus tard une solution nécessitant des littéraux de chaîne et des calculs de temps de compilation (C ++ 14 ou 17 je pense) où vous pouvez hacher les chaînes de cas au moment de la compilation et hacher la chaîne de commutateur au moment de l'exécution. Il serait peut-être intéressant pour des commutateurs très longs, mais certainement encore moins rétrocompatibles si cela importe.
Dirk Bester
Pourriez-vous partager la solution de compilation ici s'il vous plaît? Merci!
qed
12

Le problème est que, pour des raisons d'optimisation, l'instruction switch en C ++ ne fonctionne que sur les types primitifs et vous ne pouvez les comparer qu'avec les constantes de temps de compilation.

Vraisemblablement, la raison de la restriction est que le compilateur est capable d'appliquer une certaine forme d'optimisation en compilant le code vers une instruction cmp et un goto où l'adresse est calculée en fonction de la valeur de l'argument au moment de l'exécution. Étant donné que les branchements et les boucles et ne fonctionnent pas bien avec les processeurs modernes, cela peut être une optimisation importante.

Pour contourner cela, je crains que vous n'ayez à recourir à des déclarations if.

tomjen
la source
Une version optimisée d'une instruction switch qui peut fonctionner avec des chaînes est certainement possible. Le fait qu'ils ne peuvent pas réutiliser le même chemin de code qu'ils utilisent pour les types primitifs ne signifie pas qu'ils ne peuvent pas faire std::stringet les autres premiers citoyens dans le langage et les soutenir dans l'instruction switch avec un algorithme efficace.
ceztko
10

std::map + Modèle lambdas C ++ 11 sans énumérations

unordered_mappour le potentiel amorti O(1): Quelle est la meilleure façon d'utiliser 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;
    }
}

Production:

one 1
two 2
three 3
foobar -1

Utilisation à l'intérieur de méthodes avec static

Pour utiliser ce modèle efficacement à l'intérieur des classes, initialisez la carte lambda de manière statique, sinon vous payez à O(n)chaque fois pour la construire à partir de zéro.

Ici, nous pouvons nous en sortir avec l' {}initialisation d'une staticvariable de méthode: Variables statiques dans les méthodes de classe , mais nous pourrions également utiliser les méthodes décrites dans: constructeurs statiques en C ++? J'ai besoin d'initialiser des objets statiques privés

Il était nécessaire de transformer la capture du contexte lambda [&]en argument, ou cela n'aurait pas été défini: const statique lambda automatique utilisé avec capture par référence

Exemple qui produit la même sortie que ci-dessus:

#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 郝海东 冠状 病 六四 事件 法轮功
la source
3
Notez qu'il existe une différence entre cette approche et une switchdéclaration. La duplication des valeurs de cas dans une switchinstruction est un échec de compilation. L'utilisation std::unordered_mapsilencieuse accepte les valeurs en double.
D.Shawley
6

En C ++ et C, les commutateurs ne fonctionnent que sur les types entiers. Utilisez plutôt une échelle if else. C ++ aurait évidemment pu implémenter une sorte de déclaration swich pour les chaînes - je suppose que personne n'en a pensé la peine, et je suis d'accord avec elles.


la source
d'accord, mais savez-vous ce qui a rendu cela impossible à utiliser
yesraaj
L'histoire? Activer des nombres réels, des pointeurs et des structures (les seuls autres types de données de C) ne fait pas de sanse, donc C l'a limité aux entiers.
Surtout si vous activez des classes qui permettent des conversions implicites, vous passerez un très bon moment une fois.
sharptooth
6

Pourquoi pas? Vous pouvez utiliser une implémentation de commutateur avec une syntaxe équivalente et la même sémantique. Le Clangage n'a pas du tout d'objets et de chaînes d'objets, mais les chaînes en Csont des chaînes terminées par des valeurs nulles référencées par un pointeur. Le C++langage a la possibilité de faire des fonctions de surcharge pour la comparaison d'objets ou la vérification des égalités d'objets. Comme Cen C++est suffisamment souple pour avoir un tel interrupteur pour les chaînes de C langue et pour les objets de tout type que le soutien ou vérifier l' égalité comparaison de la C++langue. Et moderne C++11permet d'avoir cette implémentation de commutateur suffisamment efficace.

Votre code sera comme ceci:

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"

Il est possible d'utiliser des types plus compliqués par exemple std::pairsou des structures ou classes qui prennent en charge les opérations d'égalité (ou des comarisions pour le mode rapide ).

Caractéristiques

  • tout type de données qui prennent en charge les comparaisons ou vérifient l'égalité
  • possibilité de construire des statemens de commutateurs imbriqués en cascade.
  • possibilité de casser ou de passer à travers les déclarations de cas
  • possibilité d'utiliser des expressions de cas non constatnt
  • possibilité d'activer le mode statique / dynamique rapide avec recherche d'arborescence (pour C ++ 11)

Les différences de Sintax avec le changement de langue sont

  • mots clés en majuscules
  • besoin de parenthèses pour l'instruction CASE
  • point-virgule ';' à la fin des déclarations n'est pas autorisé
  • deux points ':' à l'instruction CASE n'est pas autorisé
  • besoin d'un mot clé BREAK ou FALL à la fin de l'instruction CASE

Pour la C++97langue utilisée la recherche linéaire. Pour C++11et plus moderne possible d'utiliser le quickmode de recherche d'arbre wuth où la déclaration de retour dans CASE n'est plus autorisée. L' Cimplémentation du langage existe lorsque char*des comparaisons de types et de chaînes terminées par zéro sont utilisées.

En savoir plus sur cette implémentation de commutateur.

oklas
la source
6

Pour ajouter une variante en utilisant le conteneur le plus simple possible (pas besoin d'une carte ordonnée) ... Je ne me dérangerais pas avec une énumération - mettez simplement la définition du conteneur juste avant le commutateur pour qu'il soit facile de voir quel nombre représente quel cas.

Cela effectue une recherche hachée dans le unordered_mapet utilise l'associé intpour piloter l'instruction switch. Devrait être assez rapide. Notez qu'il atest utilisé à la place de [], car j'ai créé ce conteneur const. L'utilisation []peut être dangereuse - si la chaîne n'est pas dans la carte, vous créerez un nouveau mappage et vous pourriez vous retrouver avec des résultats indéfinis ou une carte en croissance continue.

Notez que la at()fonction lèvera une exception si la chaîne n'est pas dans la carte. Donc, vous voudrez peut-être d'abord tester en utilisant 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 version avec un test pour une chaîne non définie suit:

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
la source
4

Je pense que la raison en est qu'en C, les chaînes ne sont pas des types primitifs, comme l'a dit tomjen, pensez dans une chaîne comme un tableau de caractères, donc vous ne pouvez pas faire des choses comme:

switch (char[]) { // ...
switch (int[]) { // ...
grilix
la source
3
Sans le rechercher, un tableau de caractères dégénérerait probablement en un char *, qui se convertit directement en un type intégral. Donc, il pourrait bien compiler, mais il ne fera certainement pas ce que vous voulez.
David Thornley
3

En c ++, les chaînes ne sont pas des citoyens de première classe. Les opérations de chaîne sont effectuées via une bibliothèque standard. Je pense que c'est la raison. En outre, C ++ utilise l'optimisation de table de branche pour optimiser les instructions de casse de commutateur. Jetez un œil au lien.

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

chappar
la source
2

En C ++, vous ne pouvez utiliser qu'une instruction switch sur int et char

CodeMonkey1313
la source
3
Un char se transforme également en int.
strager
Les pointeurs le peuvent aussi. Cela signifie que vous pouvez parfois compiler quelque chose qui aurait du sens dans une langue différente, mais cela ne fonctionnera pas correctement.
David Thornley
Vous pouvez réellement utiliser longet long long, qui ne se transformera pas en int. Il n'y a aucun risque de troncature là-bas.
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
la source
4
Bien que ce code puisse répondre à la question, fournir un contexte supplémentaire concernant pourquoi et / ou comment ce code répond à la question améliore sa valeur à long terme.
Benjamin W.
0

dans de nombreux cas, vous pouvez faire un travail supplémentaire en tirant le premier caractère de la chaîne et en l'activant. peut finir par devoir faire un switch imbriqué sur charat (1) si vos cas commencent avec la même valeur. toute personne lisant votre code apprécierait un indice, car la plupart d'entre eux chercheraient juste si-sinon-si

Marshall Taylor
la source
0

Solution de contournement plus fonctionnelle au problème du commutateur:

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
la source
-1

Vous ne pouvez pas utiliser de chaîne dans le cas du commutateur. Seuls les caractères int et char sont autorisés. Au lieu de cela, vous pouvez essayer enum pour représenter la chaîne et l'utiliser dans le bloc de cas de commutateur comme

enum MyString(raj,taj,aaj);

Utilisez-le dans l'instruction swich case.

anil
la source
-1

Les commutateurs ne fonctionnent qu'avec des types intégraux (int, char, bool, etc.). Pourquoi ne pas utiliser une carte pour associer une chaîne à un nombre, puis utiliser ce numéro avec le commutateur?

derpface
la source
-2

C'est parce que C ++ transforme les commutateurs en tables de saut. Il effectue une opération triviale sur les données d'entrée et saute à la bonne adresse sans comparer. Puisqu'une chaîne n'est pas un nombre, mais un tableau de nombres, C ++ ne peut pas en créer une table de saut.

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

(code de wikipedia https://en.wikipedia.org/wiki/Branch_table )

Jean-Luc Nacif Coelho
la source
4
C ++ ne nécessite aucune implémentation particulière de sa syntaxe. Un naïf cmp/ jccmise en œuvre peut être tout aussi valable selon la norme C ++.
Ruslan