Sintaks yang valid, tetapi tidak bernilai pada sakelar kasus?

207

Melalui kesalahan ketik kecil, saya tidak sengaja menemukan konstruk ini:

int main(void) {
    char foo = 'c';

    switch(foo)
    {
        printf("Cant Touch This\n");   // This line is Unreachable

        case 'a': printf("A\n"); break;
        case 'b': printf("B\n"); break;
        case 'c': printf("C\n"); break;
        case 'd': printf("D\n"); break;
    }

    return 0;
}

Tampaknya pernyataan printfdi bagian atas switchitu valid, tetapi juga sama sekali tidak terjangkau.

Saya mendapat kompilasi bersih, bahkan tanpa peringatan tentang kode yang tidak dapat dijangkau, tetapi ini sepertinya tidak ada gunanya.

Haruskah kompiler menandai ini sebagai kode yang tidak terjangkau?
Apakah ini ada gunanya sama sekali?

kasar
sumber
51
GCC memiliki bendera khusus untuk ini. Ini-Wswitch-unreachable
Eli Sadoff
57
"Apakah ini ada gunanya sama sekali?" Nah, Anda bisa gotomasuk dan keluar dari bagian yang tidak terjangkau, yang mungkin berguna untuk berbagai peretasan.
HolyBlackCat
12
@HolyBlackCat Bukankah itu untuk semua kode yang tidak terjangkau?
Eli Sadoff
28
@EliSadoff Memang. Saya kira itu tidak melayani tujuan khusus . Saya yakin itu diperbolehkan hanya karena tidak ada alasan untuk melarangnya. Bagaimanapun, switchini hanya sebuah kondisional gotodengan banyak label. Ada batasan yang kurang lebih sama pada tubuh itu seperti yang Anda miliki pada blok kode reguler yang diisi dengan label goto.
HolyBlackCat
16
Layak menunjukkan bahwa contoh @MooingDuck adalah varian pada perangkat Duff ( en.wikipedia.org/wiki/Duff's_device )
Michael Anderson

Jawaban:

226

Mungkin bukan yang paling berguna, tetapi tidak sepenuhnya tidak berharga. Anda dapat menggunakannya untuk mendeklarasikan variabel lokal yang tersedia dalam switchruang lingkup.

switch (foo)
{
    int i;
case 0:
    i = 0;
    //....
case 1:
    i = 1;
    //....
}

Standar ( N1579 6.8.4.2/7) memiliki sampel berikut:

CONTOH Dalam fragmen program buatan

switch (expr)
{
    int i = 4;
    f(i);
case 0:
    i = 17;
    /* falls through into default code */
default:
    printf("%d\n", i);
}

objek yang pengenalnya iada dengan durasi penyimpanan otomatis (dalam blok) tetapi tidak pernah diinisialisasi, dan dengan demikian jika ekspresi pengontrol memiliki nilai bukan nol, panggilan ke printffungsi akan mengakses nilai yang tidak ditentukan. Demikian pula, panggilan ke fungsi ftidak dapat dihubungi.

PS BTW, sampel tidak valid kode C ++. Dalam hal itu ( N4140 6.7/3, tekankan tambang):

Program yang melompati 90 dari titik di mana variabel dengan durasi penyimpanan otomatis tidak dalam ruang lingkup ke titik di mana ruang lingkupnya tidak terbentuk kecuali variabel tersebut memiliki tipe skalar , tipe kelas dengan konstruktor standar sepele dan destruktor sepele, versi yang memenuhi syarat cv dari salah satu jenis ini, atau larik salah satu dari jenis sebelumnya dan dinyatakan tanpa penginisialisasi (8.5).


90) Transfer dari kondisi switchpernyataan ke label kasus dianggap lompatan dalam hal ini.

Jadi mengganti int i = 4;dengan int i;membuatnya menjadi C ++ yang valid.

AlexD
sumber
3
"... tapi tidak pernah diinisialisasi ..." Sepertinya idiinisialisasi ke 4, apa yang saya lewatkan?
yano
7
Perhatikan bahwa jika variabelnya static, itu akan diinisialisasi ke nol, jadi ada penggunaan yang aman untuk ini juga.
Leushenko
23
@Yano Kami selalu melompati i = 4;inisialisasi, jadi itu tidak pernah terjadi.
AlexD
12
Hah tentu saja! ... inti dari pertanyaan ... Ya ampun. Keinginan kuat untuk menghapus kebodohan ini
yano
1
Bagus! Kadang-kadang saya membutuhkan variabel temp di dalam case, dan selalu harus menggunakan nama yang berbeda di masing case- masing atau mendefinisikannya di luar saklar.
SJuan76
98

Apakah ini ada gunanya sama sekali?

Iya. Jika alih-alih pernyataan, Anda meletakkan deklarasi di depan label pertama, ini bisa masuk akal:

switch (a) {
  int i;
case 0:
  i = f(); g(); h(i);
  break;
case 1:
  i = g(); f(); h(i);
  break;
}

Aturan untuk deklarasi dan pernyataan dibagikan untuk blok secara umum, jadi aturan yang sama yang memungkinkan hal itu juga memungkinkan pernyataan di sana.


Layak disebutkan juga bahwa jika pernyataan pertama adalah konstruk loop, label kasus mungkin muncul di tubuh loop:

switch (i) {
  for (;;) {
    f();
  case 1:
    g();
  case 2:
    if (h()) break;
  }
}

Tolong jangan menulis kode seperti ini jika ada cara yang lebih mudah dibaca untuk menulisnya, tetapi itu benar-benar valid, dan f()panggilan itu dapat dijangkau.


sumber
@MatthieuM Duff's Device memang memiliki label kasus di dalam sebuah loop, tetapi dimulai dengan label case sebelum loop.
2
Saya tidak yakin apakah saya harus memilih untuk contoh yang menarik atau downvote untuk kegilaan total menulis ini dalam program nyata :). Selamat untuk menyelam ke jurang dan kembali utuh.
Liviu T.
@ChemicalEngineer: Jika kode tersebut merupakan bagian dari loop, seperti di Duff's Device, { /*code*/ switch(x) { } }mungkin terlihat lebih bersih tetapi juga salah .
Ben Voigt
39

Ada penggunaan yang terkenal dari Perangkat Duff ini .

int n = (count+3)/4;
switch (count % 4) {
  do {
    case 0: *to = *from++;
    case 3: *to = *from++;
    case 2: *to = *from++;
    case 1: *to = *from++;
  } while (--n > 0);
}

Di sini kita menyalin buffer yang ditunjuk oleh fromke buffer yang ditunjuk oleh to. Kami menyalin countcontoh data.

Itu do{}while() pernyataan dimulai sebelum pertama caselabel, dan caselabel yang tertanam dalam do{}while().

Ini mengurangi jumlah cabang kondisional pada akhir do{}while()loop yang ditemui oleh kira-kira faktor 4 (dalam contoh ini; konstanta dapat diubah dengan nilai apa pun yang Anda inginkan).

Sekarang, pengoptimal kadang-kadang dapat melakukan ini untuk Anda (terutama jika mereka mengoptimalkan streaming / petunjuk vektor), tetapi tanpa optimasi profil dipandu mereka tidak bisa tahu apakah Anda mengharapkan loop menjadi besar atau tidak.

Secara umum, deklarasi variabel dapat terjadi di sana dan digunakan dalam setiap kasus, tetapi berada di luar ruang lingkup setelah sakelar berakhir. (perhatikan inisialisasi apa pun akan dilewati)

Selain itu, aliran kontrol yang tidak spesifik sakelar dapat membawa Anda ke bagian blok sakelar tersebut, seperti yang diilustrasikan di atas, atau dengan a goto.

Yakk - Adam Nevraumont
sumber
2
Tentu saja, ini masih mungkin dilakukan tanpa mengizinkan pernyataan di atas kasus pertama, karena urutan do {dan case 0:tidak masalah, keduanya berfungsi untuk menempatkan target melompat pada yang pertama *to = *from++;.
Ben Voigt
1
@ BenVoigt Saya berpendapat bahwa menempatkan do {lebih mudah dibaca. Ya, berdebat tentang keterbacaan untuk Perangkat Duff adalah bodoh dan tidak ada gunanya dan sepertinya cara mudah untuk menjadi gila.
Dana Gugatan Monica
1
@QPaysTaxes Anda harus memeriksa Simon Tatham ini Coroutines di C . Atau mungkin tidak.
Jonas Schäfer
@ JonasSchäfer Menariknya, pada dasarnya itulah yang akan dilakukan C ++ 20 coroutine untuk Anda.
Yakk - Adam Nevraumont
15

Dengan asumsi Anda menggunakan gcc di Linux, itu akan memberi Anda peringatan jika Anda menggunakan versi 4.4 atau yang lebih lama.

Opsi -Wunreachable-code telah dihapus di gcc 4.4 dan seterusnya.

16 ton
sumber
Setelah mengalami masalah secara langsung selalu membantu!
ton
@JonathanLeffler: Masalah umum dari peringatan gcc yang rentan terhadap serangkaian pass optimasi tertentu yang dipilih masih benar, sayangnya, dan membuat pengalaman pengguna yang buruk. Benar-benar menjengkelkan memiliki bangunan Debug yang bersih diikuti oleh rilis yang gagal: /
Matthieu M.
@ MatthieuM .: Tampaknya peringatan seperti itu akan sangat mudah dideteksi dalam kasus yang melibatkan analisis gramatikal daripada semantik [misalnya kode mengikuti "jika" yang memiliki pengembalian di kedua cabang], dan membuatnya lebih mudah untuk menahan "gangguan" peringatan. Di sisi lain, ada beberapa kasus di mana berguna untuk memiliki kesalahan atau peringatan dalam rilis build yang tidak ada dalam build debug (jika tidak ada yang lain, di tempat-tempat di mana hack yang dimasukkan untuk debugging seharusnya dibersihkan untuk melepaskan).
supercat
1
@ MatthieuM .: Jika analisis semantik yang signifikan diperlukan untuk menemukan bahwa variabel tertentu akan selalu salah di beberapa tempat dalam kode, kode yang tergantung pada variabel yang benar akan ditemukan tidak dapat dijangkau hanya jika analisis tersebut dilakukan. Di sisi lain, saya akan mempertimbangkan pemberitahuan bahwa kode seperti itu tidak dapat dijangkau dengan cara yang berbeda dari peringatan tentang kode yang tidak dapat dijangkau secara sintaksis, karena seharusnya sepenuhnya normal jika berbagai kondisi dimungkinkan dengan beberapa konfigurasi proyek tetapi tidak pada yang lain. Kadang-kadang mungkin bermanfaat bagi programmer ...
supercat
1
... untuk mengetahui mengapa beberapa konfigurasi menghasilkan kode yang lebih besar daripada yang lain [misalnya karena seorang kompiler dapat menganggap suatu kondisi sebagai tidak mungkin dalam satu konfigurasi tetapi tidak yang lain] tetapi itu tidak berarti bahwa ada sesuatu yang "salah" dengan kode yang dapat dioptimalkan dalam mode itu dengan beberapa konfigurasi.
supercat
11

Tidak hanya untuk deklarasi variabel tetapi juga melompat lanjutan. Anda dapat menggunakannya dengan baik jika dan hanya jika Anda tidak rentan terhadap kode spageti.

int main()
{
    int i = 1;
    switch(i)
    {
        nocase:
        printf("no case\n");

        case 0: printf("0\n"); break;
        case 1: printf("1\n"); goto nocase;
    }
    return 0;
}

Cetakan

1
no case
0 /* Notice how "0" prints even though i = 1 */

Perlu dicatat bahwa switch-case adalah salah satu klausa aliran kontrol tercepat. Jadi pasti sangat fleksibel untuk programmer, yang terkadang melibatkan kasus seperti ini.

Sanchke Dellowar
sumber
Dan apa perbedaan antara nocase:dan default:?
i486
@ i486 Saat i=4tidak terpicu nocase.
Sanchke Dellowar
@SanchkeDellowar itulah yang saya maksud.
njzk2
Mengapa sih orang bisa melakukan itu daripada hanya menempatkan case 1 sebelum case 0 dan menggunakan fallthrough?
Jonas Schäfer
@JonasWielicki Dalam tujuan ini, Anda bisa melakukannya. Tetapi kode ini hanyalah etalase dari apa yang bisa dilakukan.
Sanchke Dellowar
11

Perlu dicatat, bahwa hampir tidak ada batasan struktural pada kode dalam switchpernyataan, atau di mana case *:label ditempatkan dalam kode ini *. Ini membuat trik pemrograman seperti perangkat duff menjadi mungkin, salah satu implementasi yang mungkin terlihat seperti ini:

int n = ...;
int iterations = n/8;
switch(n%8) {
    while(iterations--) {
        sum += *ptr++;
        case 7: sum += *ptr++;
        case 6: sum += *ptr++;
        case 5: sum += *ptr++;
        case 4: sum += *ptr++;
        case 3: sum += *ptr++;
        case 2: sum += *ptr++;
        case 1: sum += *ptr++;
        case 0: ;
    }
}

Anda lihat, kode antara switch(n%8) {dan case 7:label pasti dapat dijangkau ...


* Sebagai supercat untungnya ditunjukkan dalam komentar : Sejak C99, baik gotolabel maupun tidak (baik itu case *:label atau tidak) dapat muncul dalam lingkup deklarasi yang berisi deklarasi VLA. Jadi tidak benar untuk mengatakan bahwa tidak ada batasan struktural pada penempatan case *:label. Namun, perangkat Duff lebih dulu daripada standar C99, dan itu tidak bergantung pada VLA. Namun demikian, saya merasa harus memasukkan "hampir" ke dalam kalimat pertama saya karena ini.

cmaster - mengembalikan monica
sumber
Penambahan array panjang variabel menyebabkan pengenaan pembatasan struktural yang terkait dengannya.
supercat
@supercat Batasan seperti apa?
cmaster - mengembalikan monica
1
Baik label gotomaupun tidak switch/case/defaultdapat muncul dalam lingkup objek atau tipe yang dinyatakan berbeda-beda. Ini secara efektif berarti bahwa jika suatu blok memuat deklarasi objek atau tipe array panjang variabel, label apa pun harus mendahului deklarasi tersebut. Ada sedikit verbiage yang membingungkan dalam Standar yang akan menyarankan bahwa dalam beberapa kasus cakupan deklarasi VLA meluas ke keseluruhan pernyataan switch; lihat stackoverflow.com/questions/41752072/… untuk pertanyaan saya tentang itu.
supercat
@supercat: Anda hanya salah paham tentang kata-kata itu (yang saya duga adalah alasan Anda menghapus pertanyaan Anda). Ini membebankan persyaratan pada ruang lingkup di mana VLA dapat didefinisikan. Itu tidak memperluas ruang lingkup itu, itu hanya membuat definisi VLA tertentu tidak valid.
Keith Thompson
@KeithThompson: Ya, saya salah paham. Penggunaan aneh dari present tense dalam catatan kaki membuat hal-hal membingungkan, dan saya pikir konsep itu bisa lebih baik dinyatakan sebagai larangan: "Pernyataan peralihan yang badannya berisi deklarasi VLA di dalamnya tidak boleh menyertakan sakelar atau label kasus di dalam ruang lingkup deklarasi VLA ".
supercat
10

Anda mendapat jawaban terkait dengan opsi yang diperlukangcc -Wswitch-unreachable untuk menghasilkan peringatan, jawaban ini adalah untuk menguraikan kegunaan / kelayakan .

Mengutip langsung dari C11, bab §6.8.4.2, ( penekanan pada saya )

switch (expr)
{
int i = 4;
f(i);
case 0:
i = 17;
/* falls through into default code */
default:
printf("%d\n", i);
}

objek yang pengenalnya iada dengan durasi penyimpanan otomatis (dalam blok) tetapi tidak pernah diinisialisasi , dan dengan demikian jika ekspresi pengontrol memiliki nilai bukan nol, panggilan ke printf fungsi akan mengakses nilai yang tidak ditentukan. Demikian pula, panggilan ke fungsi ftidak dapat dihubungi.

Yang sangat jelas. Anda bisa menggunakan ini untuk mendefinisikan variabel switchcakupan lokal yang hanya tersedia dalam lingkup pernyataan.

Sourav Ghosh
sumber
9

Dimungkinkan untuk mengimplementasikan "loop setengah" dengan itu, meskipun itu mungkin bukan cara terbaik untuk melakukannya:

char password[100];
switch(0) do
{
  printf("Invalid password, try again.\n");
default:
  read_password(password, sizeof(password));
} while (!is_valid_password(password));
celtschk
sumber
@ Richard II Apakah ini permainan kata atau apa? Tolong jelaskan.
Dancia
1
@Dancia Dia mengatakan bahwa ini jelas bukan cara terbaik untuk melakukan sesuatu seperti ini, dan "mungkin tidak" adalah sesuatu yang meremehkan.
Dana Gugatan Monica