Warum kann die switch-Anweisung nicht auf Strings angewendet werden?

Lesezeit: 12 Minuten

Benutzer-Avatar
jaraaj

Das Kompilieren des folgenden Codes gibt die Fehlermeldung: type illegal.

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

Sie können auch keine Zeichenfolge verwenden switch oder case. Wieso den? Gibt es eine Lösung, die gut funktioniert, um eine Logik ähnlich wie beim Einschalten von Zeichenfolgen zu unterstützen?

  • Gibt es eine Boost-Alternative, die den Kartenaufbau hinter einem MACRO versteckt?

    – Balki

    2. März 2012 um 15:07 Uhr

  • @balki Ich bin mir bei Boost nicht sicher, aber es ist einfach, solche Makros zu schreiben. Im Fall von Qt können Sie die Zuordnung dann mit ausblenden QMetaEnum

    – phuklv

    19. Januar 2018 um 8:44 Uhr

Der Grund dafür hat mit dem Typensystem zu tun. C/C++ unterstützt Strings nicht wirklich als Typ. Es unterstützt zwar die Idee eines konstanten Zeichenarrays, versteht aber den Begriff einer Zeichenfolge nicht wirklich vollständig.

Um den Code für eine switch-Anweisung zu generieren, muss der Compiler verstehen, was es bedeutet, wenn zwei Werte gleich sind. Für Elemente wie ints und enums ist dies ein trivialer Bitvergleich. Aber wie soll der Compiler 2 Stringwerte vergleichen? Groß-/Kleinschreibung beachten, unsensibel, kulturbewusst usw. Ohne ein vollständiges Bewusstsein für eine Zeichenfolge kann dies nicht genau beantwortet werden.

Darüber hinaus werden C/C++-Switch-Anweisungen normalerweise als generiert Zweigtische. Es ist nicht annähernd so einfach, eine Verzweigungstabelle für einen Schalter im String-Stil zu erstellen.

  • Das Verzweigungstabellenargument sollte nicht gelten – das ist nur ein möglicher Ansatz, der einem Compilerautor zur Verfügung steht. Für einen Produktionscompiler muss man je nach Komplexität des Wechsels häufig mehrere Ansätze verwenden.

    – Sockel

    16. März 2009 um 13:32 Uhr

  • @plinth, ich habe es hauptsächlich aus historischen Gründen dort platziert. Viele der „Warum macht C/C++ das?“-Fragen können leicht durch die Historie des Compilers beantwortet werden. Zu der Zeit, als sie es geschrieben haben, war C eine verherrlichte Assemblierung und daher war switch wirklich ein praktischer Zweigtisch.

    – JaredPar

    16. März 2009 um 13:34 Uhr

  • Ich stimme ab, weil ich nicht verstehe, wie der Compiler weiß, wie man 2 Zeichenfolgenwerte in if-Anweisungen vergleicht, aber vergiss, wie man dasselbe in switch-Anweisungen macht.

    Benutzer955249

    12. Juni 2012 um 8:45 Uhr

  • Ich glaube nicht, dass die ersten 2 Absätze gültige Gründe sind. Vor allem seit C++14 wann std::string Literale wurden hinzugefügt. Es ist meist historisch. Aber ein Problem, das mir in den Sinn kommt, ist das mit dem Weg switch funktioniert derzeit, duplizieren cases muss zur Kompilierzeit erkannt werden; Dies ist jedoch für Zeichenfolgen möglicherweise nicht so einfach (unter Berücksichtigung der Auswahl des Gebietsschemas zur Laufzeit usw.). Ich nehme an, dass so etwas erfordern müsste constexpr Fälle, oder fügen Sie unspezifiziertes Verhalten hinzu (nie etwas, was wir tun möchten).

    – MM

    11. Juni 2015 um 23:37 Uhr


  • Es gibt eine klare Definition, wie man zwei vergleicht std::string Werte oder sogar ein std::string mit einem const char-Array (nämlich durch die Verwendung von operator==) gibt es keinen technischen Grund, der den Compiler daran hindern würde, eine switch-Anweisung für jeden Typ zu generieren, der diesen Operator bereitstellt. Es würde einige Fragen zu Dingen wie der Lebensdauer der Etiketten aufwerfen, aber alles in allem ist dies in erster Linie eine Entscheidung für das Sprachdesign, keine technische Schwierigkeit.

    – MikeMB

    15. November 2017 um 15:51 Uhr

Wie bereits erwähnt, erstellen Compiler gerne Nachschlagetabellen, die optimieren switch Anweisungen wann immer möglich in der Nähe von O(1) Timing. Kombinieren Sie dies mit der Tatsache, dass die C++-Sprache keinen Zeichenfolgentyp hat – std::string ist Teil der Standardbibliothek, die nicht Teil der Sprache an sich ist.

Ich werde eine Alternative anbieten, die Sie vielleicht in Betracht ziehen möchten, ich habe sie in der Vergangenheit mit gutem Erfolg eingesetzt. Anstatt den String selbst umzuschalten, schalten Sie das Ergebnis einer Hash-Funktion um, die den String als Eingabe verwendet. Ihr Code ist fast so klar wie das Umschalten der Zeichenfolge, wenn Sie einen vordefinierten Satz von Zeichenfolgen verwenden:

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

Es gibt eine Menge offensichtlicher Optimierungen, die ziemlich genau dem folgen, was der C-Compiler mit einer switch-Anweisung machen würde … lustig, wie das passiert.

  • Das ist wirklich enttäuschend, weil Sie nicht wirklich hashen. Mit modernem C++ können Sie tatsächlich zur Kompilierzeit mit einer constexpr-Hash-Funktion hashen. Ihre Lösung sieht sauber aus, hat aber leider all das Böse, wenn die Leiter los ist. Die folgenden Kartenlösungen wären besser und vermeiden auch den Funktionsaufruf. Zusätzlich können Sie durch die Verwendung von zwei Karten auch Text für die Fehlerprotokollierung eingebaut haben.

    – Dirk Beste

    20. Mai 2016 um 18:18 Uhr

  • Sie können die Aufzählung auch mit Lambdas vermeiden: stackoverflow.com/a/42462552/895245

    – Ciro Santilli Путлер Капут 六四事

    17. März 2018 um 9:16 Uhr

  • Könnte Hashit eine constexpr-Funktion sein? Vorausgesetzt, Sie übergeben ein const char * anstelle eines std::string.

    – Viktor Stein

    27. März 2018 um 17:19 Uhr

  • Aber wieso? Sie können jederzeit die Ausführung der if-Anweisung auf einem Schalter verwenden. Beide haben minimale Auswirkungen, aber die Leistungsvorteile mit einem Schalter werden durch die if-else-Suche gelöscht. Allein die Verwendung eines if-else sollte etwas schneller sein, aber was noch wichtiger ist, deutlich kürzer.

    – Zoe steht mit der Ukraine

    27. Mai 2020 um 19:36 Uhr

  • Ich stimme @Zoe zu, was ist der Sinn all dieses zusätzlichen Codes, wenn ein einfaches if/else ausreicht. Wenn ich in der Praxis auf dieses Beispiel stoßen würde, würde ich es löschen und in ein if else umgestalten, das für Teams besser lesbar und nachhaltiger ist. Sogar die kleinere, einfachere Version davon, die eine constexpr-Hash-Funktion verwendet, ist hässlich und unlesbar.

    – Sonny Parlin

    28. Januar um 22:08 Uhr

Benutzer-Avatar
Nick

C++

constexpr-Hash-Funktion:

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
}

Aktualisieren:

Das obige Beispiel ist C++11. Dort constexpr Funktion muss mit Einzelanweisung sein. Dies wurde in den nächsten C++-Versionen gelockert.

In C++14 und C++17 können Sie folgende Hash-Funktion verwenden:

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

Auch C++17 haben std::string_viewsodass Sie es anstelle von verwenden können const char *.

In C++20 können Sie versuchen, dies zu verwenden consteval.

  • Sie müssen sicherstellen, dass keiner Ihrer Fälle den gleichen Wert hat. Und selbst dann haben Sie möglicherweise einige Fehler, bei denen andere Zeichenfolgen, die beispielsweise den gleichen Wert wie hash (“one”) haben, fälschlicherweise das erste “etwas” in Ihrem Schalter tun.

    – David Ljung Madison Stellar

    27. April 2018 um 21:54 Uhr

  • Ich weiß, aber wenn es auf den gleichen Wert hasht, wird es nicht kompiliert und Sie werden es rechtzeitig bemerken.

    – Nick

    29. April 2018 um 6:52 Uhr

  • Guter Punkt – aber das löst nicht die Hash-Kollision für andere Zeichenfolgen, die nicht Teil Ihres Schalters sind. In einigen Fällen spielt das vielleicht keine Rolle, aber wenn dies eine generische “go-to” -Lösung wäre, könnte ich mir vorstellen, dass es irgendwann ein Sicherheitsproblem oder ähnliches gibt.

    – David Ljung Madison Stellar

    30. April 2018 um 22:49 Uhr


  • Sie können eine hinzufügen operator "" um den Code schöner zu machen. constexpr inline unsigned int operator "" _(char const * p, size_t) { return hash(p); } Und benutze es gerne case "Peter"_: break; Demo

    – Hase1039

    13. Februar 2019 um 17:32 Uhr


  • Das ist wirklich super cool für Switch-Anweisungen, danke, dass du diese Lösung gepostet hast!

    – Ray Hulha

    29. März 2021 um 15:10 Uhr

Benutzer-Avatar
Dirk Beste

C++ 11 Update von @MarmouCorp oben aber anscheinend nicht http://www.codeguru.com/cpp/cpp/cpp_mfc/article.php/c4067/Switch-on-Strings-in-C.htm

Verwendet zwei Maps, um zwischen den Zeichenfolgen und der Klassenaufzählung zu konvertieren (besser als eine einfache Aufzählung, da ihre Werte darin enthalten sind, und umgekehrte Suche nach netten Fehlermeldungen).

Die Verwendung von static im Codeguru-Code ist mit Compiler-Unterstützung für Initialisierungslisten möglich, was VS 2013 plus bedeutet. gcc 4.8.1 war damit einverstanden, nicht sicher, wie viel weiter hinten es kompatibel wäre.

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

Das Problem ist, dass die switch-Anweisung in C++ aus Optimierungsgründen nur auf primitiven Typen funktioniert und Sie sie nur mit Kompilierzeitkonstanten vergleichen können.

Vermutlich liegt der Grund für die Einschränkung darin, dass der Compiler in der Lage ist, eine Form der Optimierung anzuwenden, indem er den Code in eine cmp-Anweisung und ein goto kompiliert, wobei die Adresse basierend auf dem Wert des Arguments zur Laufzeit berechnet wird. Da Verzweigungen und Schleifen mit modernen CPUs nicht gut funktionieren, kann dies eine wichtige Optimierung sein.

Um dies zu umgehen, müssen Sie leider auf if-Anweisungen zurückgreifen.

  • Eine optimierte Version einer switch-Anweisung, die mit Strings arbeiten kann, ist durchaus möglich. Die Tatsache, dass sie denselben Codepfad, den sie für primitive Typen verwenden, nicht wiederverwenden können, bedeutet nicht, dass sie es nicht können std::string und andere erste Bürger in der Sprache und unterstützen sie bei der Schalteraussage mit einem effizienten Algorithmus.

    – ceztko

    6. Oktober 2018 um 13:01 Uhr

std::map + C++11-Lambdas-Muster ohne Aufzählungen

unordered_map für das Potenzial amortisiert O(1): Was ist der beste Weg, eine HashMap in C++ zu verwenden?

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

Ausgabe:

one 1
two 2
three 3
foobar -1

Verwendung innerhalb von Methoden mit static

Um dieses Muster effizient innerhalb von Klassen zu verwenden, initialisieren Sie die Lambda-Map statisch, andernfalls zahlen Sie O(n) jedes Mal, um es von Grund auf neu zu bauen.

Hier können wir mit dem durchkommen {} Initialisierung von a static Methodenvariable: Statische Variablen in Klassenmethoden , aber wir könnten auch die Methoden verwenden, die unter beschrieben sind: statische Konstruktoren in C++? Ich muss private statische Objekte initialisieren

Es war notwendig, die Lambda-Kontexterfassung zu transformieren [&] in ein Argument, oder das wäre undefiniert gewesen: const static auto lambda wird mit capture by reference verwendet

Beispiel, das die gleiche Ausgabe wie oben erzeugt:

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

  • Eine optimierte Version einer switch-Anweisung, die mit Strings arbeiten kann, ist durchaus möglich. Die Tatsache, dass sie denselben Codepfad, den sie für primitive Typen verwenden, nicht wiederverwenden können, bedeutet nicht, dass sie es nicht können std::string und andere erste Bürger in der Sprache und unterstützen sie bei der Schalteraussage mit einem effizienten Algorithmus.

    – ceztko

    6. Oktober 2018 um 13:01 Uhr

Um eine Variation mit dem einfachsten möglichen Container hinzuzufügen (keine Notwendigkeit für eine geordnete Karte) … Ich würde mich nicht um eine Aufzählung kümmern – setzen Sie einfach die Containerdefinition direkt vor den Schalter, damit Sie leicht sehen können, welche Nummer dargestellt wird Welcher Fall.

Dies führt eine gehashte Suche in der unordered_map und verwendet die zugehörigen int um die switch-Anweisung zu steuern. Sollte recht schnell gehen. Beachten Sie, dass at wird stattdessen verwendet []wie ich diesen Container gemacht habe const. Verwenden [] kann gefährlich sein – wenn die Zeichenfolge nicht in der Karte enthalten ist, erstellen Sie eine neue Zuordnung und erhalten möglicherweise undefinierte Ergebnisse oder eine kontinuierlich wachsende Karte.

Notiere dass der at() Die Funktion löst eine Ausnahme aus, wenn sich die Zeichenfolge nicht in der Karte befindet. Sie können also zuerst testen, ob Sie es verwenden 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;


}

Die Version mit einem Test für eine undefinierte Zeichenfolge folgt:

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

}

1015990cookie-checkWarum kann die switch-Anweisung nicht auf Strings angewendet werden?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy