Mengapa pernyataan beralih tidak dapat diterapkan pada string?


227

Kompilasi kode berikut dan dapatkan kesalahan dari type illegal.

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

Anda tidak dapat menggunakan string di salah satu switchatau case. Mengapa? Apakah ada solusi yang berfungsi baik untuk mendukung logika yang mirip dengan mengaktifkan string?


6
Apakah ada alternatif dorongan yang menyembunyikan konstruksi peta, di belakang MACRO?
balki

@ Balki Saya tidak yakin tentang peningkatan tetapi mudah untuk menulis makro seperti itu. Dalam kasus Qt maka Anda dapat menyembunyikan pemetaan denganQMetaEnum
phuclv

Jawaban:


189

Alasan mengapa ada hubungannya dengan sistem tipe. C / C ++ tidak benar-benar mendukung string sebagai tipe. Itu mendukung gagasan array char konstan tetapi tidak benar-benar sepenuhnya memahami gagasan string.

Untuk menghasilkan kode pernyataan switch, kompiler harus memahami apa artinya dua nilai menjadi sama. Untuk item seperti int dan enum, ini adalah perbandingan bit sepele. Tetapi bagaimana seharusnya kompiler membandingkan 2 nilai string? Peka huruf besar-kecil, tidak peka, sadar budaya, dll ... Tanpa kesadaran penuh akan string, ini tidak dapat dijawab dengan akurat.

Selain itu, pernyataan switch C / C ++ biasanya dihasilkan sebagai tabel cabang . Tidak mudah menghasilkan tabel cabang untuk sakelar gaya string.


11
Argumen tabel cabang seharusnya tidak berlaku - itu hanya satu pendekatan yang mungkin tersedia untuk penulis kompiler. Untuk kompiler produksi, kita harus sering menggunakan beberapa pendekatan tergantung pada kompleksitas switch.
alas

5
@plinth, saya letakkan di sana sebagian besar karena alasan historis. Banyak pertanyaan "mengapa C / C ++ melakukan ini" dapat dengan mudah dijawab oleh sejarah kompiler. Pada saat mereka menulisnya, C dimuliakan perakitan dan karenanya beralih benar-benar meja cabang yang nyaman.
JaredPar

114
Saya memilih karena saya tidak mengerti bagaimana bisa kompiler tahu bagaimana membandingkan 2 nilai string dalam pernyataan if tetapi lupa cara untuk melakukan hal yang sama dalam pernyataan switch.

15
Saya tidak berpikir 2 paragraf pertama adalah alasan yang valid. Terutama sejak C ++ 14 ketika std::stringliteral ditambahkan. Sebagian besar sejarah. Tapi satu masalah yang datang ke pikiran adalah bahwa dengan cara switchbekerja saat ini, menduplikasi cases harus dideteksi pada saat kompilasi; namun ini mungkin tidak mudah untuk string (mempertimbangkan pemilihan lokal run-time dan sebagainya). Saya kira hal seperti itu harus memerlukan constexprkasus, atau menambahkan perilaku yang tidak ditentukan (tidak pernah hal yang ingin kita lakukan).
MM

8
Ada definisi yang jelas tentang bagaimana membandingkan dua std::stringnilai atau bahkan std::stringdengan array char char (yaitu dengan menggunakan operator ==) tidak ada alasan teknis yang akan mencegah kompiler membuat pernyataan switch untuk semua jenis yang menyediakan operator itu. Ini akan membuka beberapa pertanyaan tentang hal-hal seperti seumur hidup dari lables tetapi semua dalam semua ini terutama merupakan keputusan desain bahasa, bukan kesulitan teknis.
MikeMB

60

Seperti disebutkan sebelumnya, kompiler suka membuat tabel pencarian yang mengoptimalkan switchpernyataan mendekati waktu O (1) bila memungkinkan. Gabungkan ini dengan fakta bahwa Bahasa C ++ tidak memiliki tipe string -std::string adalah bagian dari Perpustakaan Standar yang bukan merupakan bagian dari Bahasa itu sendiri.

Saya akan menawarkan alternatif yang mungkin ingin Anda pertimbangkan, saya sudah menggunakannya di masa lalu untuk efek yang baik. Alih-alih beralih di atas string itu sendiri, alihkan hasil dari fungsi hash yang menggunakan string sebagai input. Kode Anda akan hampir sejelas peralihan string jika Anda menggunakan serangkaian string yang telah ditentukan:

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

Ada banyak optimasi jelas yang cukup banyak mengikuti apa yang akan dilakukan oleh kompiler C dengan pernyataan switch ... lucu bagaimana itu terjadi.


15
Ini benar-benar mengecewakan karena Anda sebenarnya tidak hashing. Dengan C ++ modern Anda sebenarnya dapat melakukan hash pada waktu kompilasi menggunakan fungsi hash constexpr. Solusi Anda terlihat bersih tetapi memiliki semua yang buruk jika tangga terjadi sayangnya. Solusi peta di bawah ini akan lebih baik dan menghindari pemanggilan fungsi juga. Selain itu dengan menggunakan dua peta Anda bisa membuat teks untuk kesalahan log.
Dirk Bester


Mungkinkah hashit menjadi fungsi constexpr? Mengingat bahwa Anda meneruskan di const char * daripada std :: string.
Victor Stone

Tapi kenapa? Anda selalu menggunakan eksekusi pernyataan if di atas sakelar. Keduanya memiliki dampak minimal, tetapi keuntungan kinerja dengan sakelar terhapus oleh pencarian if-else. Hanya menggunakan if-else seharusnya sedikit lebih cepat, tetapi yang lebih penting, jauh lebih pendek.
Zoe

20

C ++

Fungsi 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
Anda harus memastikan bahwa tidak ada kasing yang memiliki nilai yang sama. Dan bahkan kemudian, Anda mungkin memiliki beberapa kesalahan di mana string lain yang hash, misalnya, nilai yang sama dengan hash ("satu") akan salah melakukan "sesuatu" pertama di saklar Anda.
David Ljung Madison Stellar

Saya tahu, tetapi jika hash memiliki nilai yang sama tidak akan dikompilasi dan Anda akan melihatnya tepat waktu.
Nick

Poin bagus - tetapi itu tidak menyelesaikan tabrakan hash untuk string lain yang bukan bagian dari switch Anda. Dalam beberapa kasus itu mungkin tidak masalah, tetapi jika ini adalah solusi "masuk" generik, saya bisa membayangkannya sebagai masalah keamanan atau sejenisnya pada beberapa titik.
David Ljung Madison Stellar

7
Anda dapat menambahkan operator ""untuk membuat kode lebih indah. constexpr inline unsigned int operator "" _(char const * p, size_t) { return hash(p); }Dan gunakan seperti case "Peter"_: break; Demo
hare1039

15

Pembaruan C ++ 11 dari tampaknya bukan @MarmouCorp di atas tetapi http://www.codeguru.com/cpp/cpp/cpp_mfc/article.php/c4067/Switch-on-Strings-in-C.htm

Menggunakan dua peta untuk mengkonversi antara string dan enum kelas (lebih baik daripada enum polos karena nilainya dicakup di dalamnya, dan membalikkan pencarian untuk pesan kesalahan yang bagus).

Penggunaan statis dalam kode codeguru dimungkinkan dengan dukungan kompiler untuk daftar penginisialisasi yang berarti VS 2013 plus. gcc 4.8.1 ok dengan itu, tidak yakin berapa jauh itu akan kompatibel.

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

Saya harus mencatat bahwa saya kemudian menemukan solusi yang membutuhkan string literal dan waktu perhitungan kompilasi (C ++ 14 atau 17 saya pikir) di mana Anda dapat hash string case pada waktu kompilasi dan hash switch string saat runtime. Akan bermanfaat untuk switch yang sangat lama mungkin tetapi tentu saja bahkan kurang kompatibel jika itu penting.
Dirk Bester

Bisakah Anda membagikan solusi waktu kompilasi di sini? Terima kasih!
qed

12

Masalahnya adalah bahwa untuk alasan optimasi, pernyataan switch di C ++ tidak berfungsi pada apa pun kecuali tipe primitif, dan Anda hanya dapat membandingkannya dengan konstanta waktu kompilasi.

Agaknya alasan pembatasan ini adalah bahwa kompiler dapat menerapkan beberapa bentuk optimisasi yang mengkompilasi kode ke satu instruksi cmp dan kebagian mana alamat tersebut dihitung berdasarkan nilai argumen saat runtime. Karena bercabang dan dan loop tidak bermain dengan baik dengan CPU modern, ini bisa menjadi optimasi penting.

Untuk menyiasatinya, saya khawatir Anda harus menggunakan pernyataan if.


Versi pernyataan peralihan yang dioptimalkan yang dapat bekerja dengan string jelas dimungkinkan. Fakta bahwa mereka tidak dapat menggunakan kembali jalur kode yang sama yang mereka gunakan untuk tipe primitif tidak berarti bahwa mereka tidak dapat membuat std::stringdan orang lain menjadi warga negara pertama dalam bahasa tersebut dan mendukung mereka dalam pergantian pernyataan dengan algoritma yang efisien.
ceztko

10

std::map + C ++ 11 pola lambdas tanpa enum

unordered_mapuntuk potensi diamortisasi O(1): Apa cara terbaik untuk menggunakan HashMap di 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;
    }
}

Keluaran:

one 1
two 2
three 3
foobar -1

Penggunaan metode dalam dengan static

Untuk menggunakan pola ini secara efisien di dalam kelas, inisialisasi peta lambda secara statis, atau Anda membayar O(n) setiap waktu untuk membangunnya dari awal.

Di sini kita bisa lolos dengan {}inisialisasi staticvariabel metode: Variabel statis dalam metode kelas , tetapi kita juga bisa menggunakan metode yang dijelaskan di: konstruktor statis di C ++? Saya perlu menginisialisasi objek statis pribadi

Itu perlu untuk mengubah penangkapan konteks lambda [&]menjadi argumen, atau yang tidak akan ditentukan: const static auto lambda digunakan dengan penangkapan dengan referensi

Contoh yang menghasilkan output yang sama seperti di atas:

#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
Perhatikan bahwa ada perbedaan antara pendekatan ini dan switchpernyataan. Duplikasi nilai kasus dalam switchpernyataan adalah kegagalan waktu kompilasi. Menggunakan std::unordered_mapdiam-diam menerima nilai duplikat.
D.Shawley

6

Dalam C ++ dan C switch hanya berfungsi pada tipe integer. Gunakan tangga yang lain sebagai gantinya. C ++ jelas bisa menerapkan semacam pernyataan swich untuk string - saya kira tidak ada yang menganggapnya berharga, dan saya setuju dengan mereka.


setuju, tetapi apakah Anda tahu apa yang membuat ini tidak mungkin untuk digunakan
yesraaj

Sejarah? Mengaktifkan bilangan real, pointer dan struct (C hanya tipe data lainnya) tidak membuat sanse, jadi C membatasinya ke integer.

Terutama jika Anda mengaktifkan kelas yang memungkinkan konversi tersirat Anda akan memiliki waktu yang sangat baik sekali.
sharptooth

6

Kenapa tidak? Anda dapat menggunakan implementasi sakelar dengan sintaks yang setara dan semantik yang sama. The Cbahasa tidak memiliki objek dan string objek sama sekali, tapi string di Cnull dihentikan string direferensikan oleh pointer. The C++bahasa memiliki kemungkinan untuk membuat fungsi yang berlebihan untuk benda perbandingan atau memeriksa benda kesetaraan. Seperti Cyang C++cukup fleksibel untuk memiliki saklar tersebut untuk string untuk C bahasa dan untuk objek dari jenis apa pun bahwa dukungan comparaison atau cek kesetaraan untuk C++bahasa. Dan modern C++11memungkinkan implementasi switch ini cukup efektif.

Kode Anda akan seperti ini:

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"

Dimungkinkan untuk menggunakan tipe yang lebih rumit misalnya std::pairsatau struct atau kelas apa saja yang mendukung operasi kesetaraan (atau komunikasi untuk cepat mode ).

fitur

  • semua jenis data yang mendukung perbandingan atau memeriksa kesetaraan
  • kemungkinan untuk membangun cascading nested switch statemens.
  • kemungkinan untuk menembus atau jatuh melalui pernyataan kasus
  • kemungkinan untuk menggunakan ekspresi case non constatnt
  • mungkin untuk mengaktifkan mode statis / dinamis cepat dengan pencarian pohon (untuk C ++ 11)

Perbedaan sintax dengan saklar bahasa adalah

  • kata kunci huruf besar
  • membutuhkan tanda kurung untuk pernyataan KASUS
  • titik koma ';' di akhir pernyataan tidak diizinkan
  • titik dua ':' di pernyataan CASE tidak diperbolehkan
  • memerlukan salah satu kata kunci BREAK atau FALL di akhir pernyataan CASE

Untuk C++97bahasa digunakan pencarian linier. Untuk C++11dan yang lebih modern, gunakan quickmode pencarian pohon di mana pernyataan kembali dalam KASUS tidak diizinkan. The Cpelaksanaan bahasa ada di manachar* jenis dan nol-dihentikan comparisions string digunakan.

Baca lebih lanjut tentang implementasi sakelar ini.


6

Untuk menambahkan variasi menggunakan wadah sesederhana mungkin (tidak perlu peta yang dipesan) ... Saya tidak akan repot dengan enum - cukup masukkan definisi wadah segera sebelum beralih sehingga akan mudah untuk melihat nomor yang diwakilkan kasus yang mana.

Ini melakukan pencarian hash di unordered_mapdan menggunakan yang terkait intuntuk mengarahkan pernyataan switch. Seharusnya cukup cepat. Catatan yang atdigunakan sebagai ganti [], karena saya sudah membuat wadah itu const. Menggunakan[] bisa berbahaya - jika string tidak ada di peta, Anda akan membuat pemetaan baru dan mungkin berakhir dengan hasil yang tidak ditentukan atau peta yang terus tumbuh.

Perhatikan bahwa at()fungsi akan mengeluarkan pengecualian jika string tidak ada di peta. Jadi, Anda mungkin ingin menguji dulu menggunakan 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;


}

Versi dengan tes untuk string yang tidak ditentukan mengikuti:

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

Saya pikir alasannya adalah bahwa dalam string C bukan tipe primitif, seperti kata tomjen, pikirkan string sebagai array char, sehingga Anda tidak dapat melakukan hal-hal seperti:

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

3
Tanpa melihatnya, sebuah array karakter kemungkinan akan berubah menjadi char *, yang mengkonversi langsung ke tipe integral. Jadi, mungkin kompilasi, tetapi tentu saja tidak akan melakukan apa yang Anda inginkan.
David Thornley

3

Dalam c ++ string bukan warga negara kelas satu. Operasi string dilakukan melalui pustaka standar. Saya pikir, itulah alasannya. Juga, C ++ menggunakan optimasi tabel cabang untuk mengoptimalkan pernyataan kasus sakelar. Lihat tautannya.

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


2

Di C ++ Anda hanya bisa menggunakan pernyataan switch pada int dan char


3
Seekor char berubah menjadi int juga.
strager

Pointer juga bisa. Itu berarti Anda kadang-kadang dapat mengkompilasi sesuatu yang masuk akal dalam bahasa yang berbeda, tetapi itu tidak akan berjalan dengan benar.
David Thornley

Anda benar-benar dapat menggunakan longdan long long, yang tidak akan berubah menjadi int. Tidak ada risiko pemotongan di sana.
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
Sementara kode ini dapat menjawab pertanyaan, memberikan konteks tambahan tentang mengapa dan / atau bagaimana kode ini menjawab pertanyaan meningkatkan nilai jangka panjangnya.
Benjamin W.

0

dalam banyak kasus, Anda dapat melakukan kerja ekstra dengan menarik karakter pertama dari string dan mengaktifkannya. mungkin akhirnya harus melakukan switch bersarang pada karakter (1) jika kasing Anda mulai dengan nilai yang sama. siapa pun yang membaca kode Anda akan menghargai petunjuk karena sebagian besar akan prob hanya jika-lain-jika


0

Solusi yang lebih fungsional untuk masalah sakelar:

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

Switch hanya berfungsi dengan tipe integral (int, char, bool, dll.). Mengapa tidak menggunakan peta untuk memasangkan string dengan angka lalu gunakan angka itu dengan sakelar?


-2

Itu karena C ++ mengubah switch menjadi tabel lompatan. Ia melakukan operasi sepele pada data input dan melompat ke alamat yang tepat tanpa membandingkan. Karena string bukan angka, tetapi array angka, C ++ tidak dapat membuat tabel lompatan darinya.

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

(kode dari wikipedia https://en.wikipedia.org/wiki/Branch_table )


4
C ++ tidak memerlukan implementasi sintaksis tertentu. Naif cmp/ jccimplementasi bisa sama validnya dengan Standar C ++.
Ruslan
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.