Ganti pernyataan: haruskah default menjadi kasus terakhir?

178

Pertimbangkan switchpernyataan berikut :

switch( value )
{
  case 1:
    return 1;
  default:
    value++;
    // fall-through
  case 2:
    return value * 2;
}

Kode ini mengkompilasi, tetapi apakah ini valid (= perilaku yang ditentukan) untuk C90 / C99? Saya belum pernah melihat kode di mana kasus default bukan kasus terakhir.

EDIT:
Seperti yang ditulis Jon Cage dan KillianDS : ini adalah kode yang sangat jelek dan membingungkan dan saya sangat menyadarinya. Saya hanya tertarik pada sintaks umum (apakah didefinisikan?) Dan output yang diharapkan.

tanascius
sumber
19
+1 Bahkan tidak pernah mempertimbangkan perilaku itu
Jamie Wong
@ Péter Török: Maksud Anda jika nilai == 2 itu akan mengembalikan 6?
Alexandre C.
4
@ Péter Török no, urutannya tidak masalah - jika nilai cocok dengan konstanta dalam label kasus apa pun, maka kontrol akan melompat ke pernyataan yang mengikuti label, jika tidak kontrol akan melompat ke pernyataan yang mengikuti label default jika ada.
Pete Kirkham
11
@ Jon Cage gototidak jahat. Pengikut kultus kargo adalah! Anda tidak dapat membayangkan sampai sejauh mana orang-orang ekstrem dapat menghindarinya gotokarena itu sangat jahat, membuat kode mereka berantakan.
Patrick Schlüter
3
Saya menggunakan gototerutama untuk mensimulasikan sesuatu seperti finallyklausa dalam fungsi, di mana sumber daya (file, memori) harus dirilis ketika berhenti, dan mengulangi untuk setiap kasus kesalahan daftar freedan closetidak membantu keterbacaan. Meskipun ada satu penggunaan gotoyang ingin saya hindari tetapi tidak bisa, adalah ketika saya ingin keluar dari lingkaran dan saya berada di switchdalam lingkaran itu.
Patrick Schlüter

Jawaban:

83

Standar C99 tidak eksplisit tentang hal ini, tetapi jika menggabungkan semua fakta, ini benar-benar valid.

A casedan defaultlabel sama dengan gotolabel. Lihat 6.8.1 Pernyataan berlabel. Yang sangat menarik adalah 6.8.1.4, yang memungkinkan Perangkat Duff yang telah disebutkan:

Pernyataan apa pun dapat didahului dengan awalan yang menyatakan pengidentifikasi sebagai nama label. Label dalam diri mereka sendiri tidak mengubah aliran kontrol, yang berlanjut tanpa hambatan di dalamnya.

Sunting : Kode dalam sakelar tidak ada yang istimewa; ini adalah blok kode normal seperti pada- ifpernyataan, dengan label lompatan tambahan. Ini menjelaskan perilaku jatuh dan mengapa breakperlu.

6.8.4.2.7 bahkan memberikan contoh:

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

Dalam fragmen program buatan objek yang pengidentifikasi saya ada dengan durasi penyimpanan otomatis (dalam blok) tetapi tidak pernah diinisialisasi, dan dengan demikian jika ekspresi pengontrol memiliki nilai bukan nol, panggilan ke fungsi printf akan mengakses nilai tak tentu. Demikian pula, panggilan ke fungsi f tidak dapat dihubungi.

Konstanta kasing harus unik dalam pernyataan sakelar:

6.8.4.2.3 Ekspresi setiap label case harus berupa ekspresi konstanta bilangan bulat dan tidak ada dua ekspresi konstanta case dalam pernyataan switch yang sama harus memiliki nilai yang sama setelah konversi. Mungkin ada paling banyak satu label default dalam pernyataan switch.

Semua kasus dievaluasi, kemudian melompat ke label default, jika diberikan:

6.8.4.2.5 Promosi bilangan bulat dilakukan pada ekspresi kontrol. Ekspresi konstan dalam setiap label kasus dikonversi ke tipe yang dipromosikan dari ekspresi pengontrol. Jika nilai yang dikonversi cocok dengan ekspresi kontrol yang dipromosikan, kontrol melompat ke pernyataan yang mengikuti label kasus yang cocok. Kalau tidak, jika ada label default, kontrol melompat ke pernyataan berlabel. Jika tidak ada ekspresi konstan case yang dikonversi cocok dan tidak ada label default, tidak ada bagian dari tubuh switch dieksekusi.

Aman
sumber
6
@HeathHunnicutt Anda jelas tidak mengerti tujuan dari contoh ini. Kode tidak dibuat oleh poster ini, tetapi diambil langsung dari standar C, sebagai ilustrasi betapa anehnya pernyataan peralihan, dan seberapa buruk praktik akan menyebabkan bug. Jika Anda repot-repot membaca teks di bawah kode, Anda akan menyadarinya.
Lundin
2
+1 untuk mengkompensasi downvote. Menurunkan seseorang dari mengutip standar C tampaknya cukup keras.
Lundin
2
@Lundin Saya tidak memilih V standar, dan saya tidak mengabaikan apa pun seperti yang Anda sarankan. Saya memilih pedagogi buruk menggunakan contoh yang buruk, dan tidak dibutuhkan. Secara khusus, contoh itu berhubungan dengan situasi yang berbeda sama sekali dari yang ditanyakan. Saya bisa melanjutkan, tetapi "terima kasih atas tanggapan Anda."
Heath Hunnicutt
12
Intel memberi tahu Anda untuk menempatkan kode paling sering terlebih dahulu dalam pernyataan beralih di Branch and Loop Reorganization untuk Mencegah Mispredicts . Saya di sini karena saya memiliki defaultkasus yang mendominasi kasus-kasus lain sekitar 100: 1, dan saya tidak tahu apakah defaultkasusnya valid atau tidak ditentukan untuk membuat kasus pertama.
jww
@ jww Saya tidak yakin apa yang Anda maksud dengan Intel. Jika Anda maksud kecerdasan saya akan menyebutnya hipotesis. Saya memiliki pemikiran yang sama, tetapi kemudian membaca menyatakan bahwa tidak seperti pernyataan if, pernyataan switch adalah akses acak. Jadi kasus terakhir tidak lebih lambat untuk dijangkau daripada yang pertama. Ini dicapai dengan hashing nilai kasus konstan. Itulah sebabnya pernyataan pergantian lebih cepat daripada jika pernyataan ketika cabang banyak.
91

Pernyataan kasus dan pernyataan default dapat terjadi dalam urutan apa pun dalam pernyataan switch. Klausa default adalah klausa opsional yang cocok jika tidak ada konstanta dalam pernyataan kasus dapat dicocokkan.

Contoh yang baik :-

switch(5) {
  case 1:
    echo "1";
    break;
  case 2:
  default:
    echo "2, default";
    break;
  case 3;
    echo "3";
    break;
}


Outputs '2,default'

sangat berguna jika Anda ingin kasus Anda disajikan dalam urutan logis dalam kode (seperti pada, tidak mengatakan kasus 1, kasus 3, kasus 2 / default) dan kasus Anda sangat panjang sehingga Anda tidak ingin mengulangi seluruh kasus kode di bagian bawah untuk default

Salil
sumber
7
Ini persis skenario di mana saya biasanya menempatkan default di suatu tempat selain akhir ... ada urutan logis untuk kasus eksplisit (1, 2, 3) dan saya ingin default berperilaku persis sama dengan salah satu kasus eksplisit yang bukan yang terakhir.
ArtOfWarfare
51

Ini valid dan sangat berguna dalam beberapa kasus.

Pertimbangkan kode berikut:

switch(poll(fds, 1, 1000000)){
   default:
    // here goes the normal case : some events occured
   break;
   case 0:
    // here goes the timeout case
   break;
   case -1:
     // some error occurred, you have to check errno
}

Intinya adalah bahwa kode di atas lebih mudah dibaca dan efisien daripada mengalir if. Anda bisa meletakkannya defaultdi akhir, tetapi tidak ada gunanya karena akan memusatkan perhatian Anda pada kasus kesalahan daripada kasus normal (yang di sini adalah defaultkasus).

Sebenarnya, itu bukan contoh yang baik, karena pollAnda tahu berapa banyak peristiwa yang paling banyak terjadi. Titik nyata saya adalah bahwa ada yang kasus dengan satu set didefinisikan dari nilai input di mana ada 'pengecualian' dan kasus normal. Jika lebih baik menempatkan pengecualian atau kasus normal di depan adalah masalah pilihan.

Dalam bidang perangkat lunak saya memikirkan kasus yang sangat umum: rekursi dengan beberapa nilai terminal. Jika Anda dapat mengekspresikannya menggunakan sakelar, defaultakan menjadi nilai biasa yang berisi panggilan rekursif dan elemen yang dibedakan (masing-masing kasing) nilai terminal. Biasanya tidak perlu fokus pada nilai terminal.

Alasan lain adalah bahwa urutan kasus dapat mengubah perilaku kode yang dikompilasi, dan itu penting untuk kinerja. Sebagian besar kompiler akan menghasilkan kode rakitan yang dikompilasi dalam urutan yang sama dengan kode yang muncul di sakelar. Itu membuat case pertama sangat berbeda dari yang lain: semua case kecuali yang pertama akan melibatkan lompatan dan yang akan mengosongkan jalur pipa prosesor. Anda mungkin memahaminya seperti prediktor cabang default untuk menjalankan kasing pertama yang muncul di sakelar. Jika suatu kasus jika jauh lebih umum daripada yang lain maka Anda memiliki alasan yang sangat baik untuk menjadikannya sebagai kasus pertama.

Membaca komentar adalah alasan spesifik mengapa poster asli mengajukan pertanyaan itu setelah membaca reorganisasi Intel Loop Cabang kompilasi tentang optimasi kode.

Maka akan menjadi beberapa arbitrase antara keterbacaan kode dan kinerja kode. Mungkin lebih baik untuk memberikan komentar untuk menjelaskan kepada pembaca di masa depan mengapa suatu kasus muncul terlebih dahulu.

Kriss
sumber
6
+1 untuk memberi contoh (baik) tanpa perilaku fallthrough.
KillianDS
1
... memikirkannya, saya tidak yakin memiliki default di atas adalah baik karena sangat sedikit orang akan mencarinya di sana. Mungkin lebih baik untuk menetapkan pengembalian ke variabel dan menangani keberhasilan di satu sisi jika dan kesalahan di sisi lain dengan pernyataan kasus.
Jon Cage
@ Jon: tulis saja. Anda menambahkan noise sintaksis tanpa manfaat keterbacaan. Dan, jika default di atas, benar-benar tidak perlu melihatnya, itu benar-benar jelas (itu bisa lebih rumit jika Anda meletakkannya di tengah).
Kriss
Omong-omong saya tidak terlalu suka sintaks C switch / case. Saya lebih suka untuk dapat menempatkan beberapa label setelah satu kasus daripada harus menempatkan beberapa label berturut-turut case. Apa yang menyedihkan adalah bahwa itu terlihat seperti gula sintaksis dan tidak akan merusak kode yang ada jika didukung.
Kriss
1
@ Kriss: Aku setengah tergoda untuk mengatakan, "Aku juga bukan programmer python!" :)
Andrew Grimm
16

ya, ini valid, dan dalam beberapa keadaan bahkan berguna. Secara umum, jika Anda tidak membutuhkannya, jangan lakukan itu.

Jens Gustedt
sumber
-1: Ini bau kejahatan bagi saya. Akan lebih baik untuk membagi kode menjadi sepasang pernyataan switch.
Jon Cage
25
@ John Cage: menempatkan saya -1 di sini tidak menyenangkan. Bukan salah saya bahwa ini adalah kode yang valid.
Jens Gustedt
hanya ingin tahu, saya ingin tahu dalam keadaan apa itu berguna?
Salil
1
-1 ditujukan pada pernyataan Anda bahwa itu berguna. Saya akan mengubahnya menjadi +1 jika Anda dapat memberikan contoh yang valid untuk mendukung klaim Anda.
Jon Cage
4
Kadang-kadang ketika beralih untuk kesalahan yang kita dapatkan kembali dari beberapa fungsi sistem. Katakanlah kita memiliki satu kasus di mana kita tahu bahwa kita harus melakukan jalan keluar yang bersih, tetapi jalan keluar yang bersih ini mungkin memerlukan beberapa baris kode yang tidak ingin kita ulangi. Tetapi misalkan kita juga memiliki banyak kode kesalahan eksotis lainnya yang tidak ingin kita tangani secara terpisah. Saya akan mempertimbangkan hanya menempatkan perror dalam case default dan membiarkannya berjalan ke case lainnya dan keluar dengan bersih. Saya tidak mengatakan Anda harus melakukannya seperti itu. Ini hanya masalah selera.
Jens Gustedt
8

Tidak ada urutan yang ditentukan dalam pernyataan switch. Anda dapat melihat kasing sebagai sesuatu seperti label bernama, seperti gotolabel. Bertentangan dengan apa yang tampaknya dipikirkan orang di sini, dalam hal nilai 2 label default tidak melompat ke. Untuk menggambarkan dengan contoh klasik, berikut adalah perangkat Duff , yang merupakan anak poster dari ekstrem switch/casedi C.

send(to, from, count)
register short *to, *from;
register count;
{
  register n=(count+7)/8;
  switch(count%8){
    case 0: do{ *to = *from++;
    case 7:     *to = *from++;
    case 6:     *to = *from++;
    case 5:     *to = *from++;
    case 4:     *to = *from++;
    case 3:     *to = *from++;
    case 2:     *to = *from++;
    case 1:     *to = *from++;
            }while(--n>0);
  }
}
Patrick Schlüter
sumber
4
Dan bagi siapa pun yang tidak terbiasa dengan perangkat Duff kode ini benar-benar tidak dapat dibaca ...
KillianDS
7

Satu skenario di mana saya akan menganggap tepat untuk memiliki 'default' yang terletak di tempat lain selain akhir dari pernyataan kasus adalah di mesin negara di mana keadaan yang tidak valid harus mengatur ulang mesin dan melanjutkan seolah-olah itu adalah keadaan awal. Sebagai contoh:

beralih (widget_state)
{
  default: / * Jatuh dari rel - reset dan lanjutkan * /
    widget_state = WIDGET_START;
    /* Gagal */
  case WIDGET_START:
    ...
    istirahat;
  huruf besar WIDGET_WHATEVER:
    ...
    istirahat;
}

pengaturan alternatif, jika keadaan tidak valid tidak boleh mereset mesin tetapi harus mudah diidentifikasi sebagai keadaan tidak valid:

beralih (widget_state) { huruf WIDGET_IDLE: widget_ready = 0; widget_hardware_off (); istirahat; case WIDGET_START: ... istirahat; huruf besar WIDGET_WHATEVER: ... istirahat; default: widget_state = WIDGET_INVALID_STATE; /* Gagal */ case WIDGET_INVALID_STATE: widget_ready = 0; widget_hardware_off (); ... melakukan apa pun yang diperlukan untuk menciptakan kondisi "aman" }

Kode di tempat lain kemudian dapat memeriksa (widget_state == WIDGET_INVALID_STATE) dan memberikan perilaku pelaporan kesalahan atau pengaturan ulang negara apa pun yang tampaknya sesuai. Misalnya, kode bilah status dapat menampilkan ikon kesalahan, dan opsi menu "mulai widget" yang dinonaktifkan di sebagian besar negara non-idle dapat diaktifkan untuk WIDGET_INVALID_STATE serta WIDGET_IDLE.

supercat
sumber
6

Menyambung dengan contoh lain: Ini bisa bermanfaat jika "default" adalah kasus yang tidak terduga, dan Anda ingin mencatat kesalahan tetapi juga melakukan sesuatu yang masuk akal. Contoh dari beberapa kode saya sendiri:

  switch (style)
  {
  default:
    MSPUB_DEBUG_MSG(("Couldn't match dash style, using solid line.\n"));
  case SOLID:
    return Dash(0, RECT_DOT);
  case DASH_SYS:
  {
    Dash ret(shapeLineWidth, dotStyle);
    ret.m_dots.push_back(Dot(1, 3 * shapeLineWidth));
    return ret;
  }
  // more cases follow
  }
Brennan Vincent
sumber
5

Ada beberapa kasus ketika Anda mengonversi ENUM ke string atau mengubah string menjadi enum jika Anda menulis / membaca ke / dari file.

Terkadang Anda perlu membuat salah satu nilai default untuk menutupi kesalahan yang dibuat dengan mengedit file secara manual.

switch(textureMode)
{
case ModeTiled:
default:
    // write to a file "tiled"
    break;

case ModeStretched:
    // write to a file "stretched"
    break;
}
Predrag Manojlovic
sumber
2

The defaultkondisi dapat menjadi suatu tempat dalam switch yang klausul kasus bisa eksis. Tidak perlu menjadi klausa terakhir. Saya telah melihat kode yang menempatkan default sebagai klausa pertama. The case 2:dijalankan secara normal, meskipun klausa default adalah di atas itu.

Sebagai tes, saya meletakkan kode sampel dalam suatu fungsi, dipanggil test(int value){}dan dijalankan:

  printf("0=%d\n", test(0));
  printf("1=%d\n", test(1));
  printf("2=%d\n", test(2));
  printf("3=%d\n", test(3));
  printf("4=%d\n", test(4));

Outputnya adalah:

0=2
1=1
2=4
3=8
4=10
Scott Thomson
sumber
1

Itu sah, tapi agak jahat. Saya menyarankan itu umumnya buruk untuk membiarkan jatuh karena dapat menyebabkan beberapa kode spaghetti yang sangat berantakan.

Ini hampir pasti lebih baik untuk memecah kasus-kasus ini menjadi beberapa pernyataan switch atau fungsi yang lebih kecil.

[edit] @ Kristopia: Contoh Anda:

Example from UCS-2 to UTF-8 conversion 

r is the destination array, 
wc is the input wchar_t  

switch(utf8_length) 
{ 
    /* Note: code falls through cases! */ 
    case 3: r[2] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x800; 
    case 2: r[1] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x0c0; 
    case 1: r[0] = wc;
}

akan lebih jelas untuk niat itu (saya pikir) jika itu ditulis seperti ini:

if( utf8_length >= 1 )
{
    r[0] = wc;

    if( utf8_length >= 2 )
    {
        r[1] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x0c0; 

        if( utf8_length == 3 )
        {
            r[2] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x800; 
        }
    }
}   

[edit2] @ristopia: Contoh kedua Anda mungkin adalah contoh terbersih dari penggunaan yang baik untuk tindak lanjut:

for(i=0; s[i]; i++)
{
    switch(s[i])
    {
    case '"': 
    case '\'': 
    case '\\': 
        d[dlen++] = '\\'; 
        /* fall through */ 
    default: 
        d[dlen++] = s[i]; 
    } 
}

..tapi secara pribadi saya akan membagi pengakuan komentar menjadi fungsinya sendiri:

bool isComment(char charInQuestion)
{   
    bool charIsComment = false;
    switch(charInQuestion)
    {
    case '"': 
    case '\'': 
    case '\\': 
        charIsComment = true; 
    default: 
        charIsComment = false; 
    } 
    return charIsComment;
}

for(i=0; s[i]; i++)
{
    if( isComment(s[i]) )
    {
        d[dlen++] = '\\'; 
    }
    d[dlen++] = s[i]; 
}
Jon Cage
sumber
2
Ada kasus di mana jatuh benar-benar ide yang bagus.
Patrick Schlüter
Contoh dari konversi UCS-2 ke UTF-8 radalah array tujuan, wcadalah wchar_t saklar input (utf8_length) {/ * Catatan: kode jatuh melalui kasus! * / case 3: r [2] = 0x80 | (wc & 0x3f); wc >> = 6; wc | = 0x800; case 2: r [1] = 0x80 | (wc & 0x3f); wc >> = 6; wc | = 0xc0; case 1: r [0] = wc; }
Patrick Schlüter
Di sini yang lain, rutin menyalin string dengan karakter melarikan diri: for(i=0; s[i]; i++) { switch(s[i]) { case '"': case '\'': case '\\': d[dlen++] = '\\'; /* fall through */ default: d[dlen++] = s[i]; } }
Patrick Schlüter
Ya, tetapi rutin ini adalah salah satu hotspot kami, ini adalah cara tercepat, portabel (kami tidak akan melakukan perakitan) untuk mengimplementasikannya. Hanya memiliki 1 tes untuk panjang UTF, Anda memiliki 2 atau bahkan 3. Selain itu, saya tidak memunculkannya, saya mengambilnya dari BSD.
Patrick Schlüter
1
Ya ada, terutama dalam konversi dalam bahasa Bulgaria dan Yunani (pada Solaris SPARC) dan teks dengan markup internal kami (yang merupakan 3 byte UTF8). Diakui, dalam toto itu tidak banyak dan telah menjadi tidak relevan sejak pembaruan perangkat keras terakhir kami, tetapi pada saat itu ditulis itu membuat beberapa perbedaan.
Patrick Schlüter