Mengapa Dentang / LLVM memperingatkan saya tentang penggunaan default dalam pernyataan switch di mana semua kasus yang disebutkan dicakup?

34

Pertimbangkan pernyataan enum dan switch berikut:

typedef enum {
    MaskValueUno,
    MaskValueDos
} testingMask;

void myFunction(testingMask theMask) {
    switch (theMask) {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
        default: {} //deal with an unexpected or uninitialized value
    }
};

Saya seorang programmer Objective-C, tetapi saya telah menulis ini dalam C murni untuk audiens yang lebih luas.

Dentang / LLVM 4.1 dengan -Weverything memperingatkan saya di baris default:

Label default dalam sakelar yang mencakup semua nilai enumerasi

Sekarang, saya bisa melihat mengapa ini ada: di dunia yang sempurna, satu-satunya nilai yang masuk dalam argumen theMaskakan berada di enum, jadi tidak ada standar yang diperlukan. Tetapi bagaimana jika beberapa hack datang dan melempar int yang tidak diinisialisasi ke dalam fungsi saya yang cantik? Fungsi saya akan disediakan sebagai setetes di perpustakaan, dan saya tidak punya kendali atas apa yang bisa masuk ke sana. Menggunakan defaultadalah cara penanganan yang sangat rapi.

Mengapa para dewa LLVM menganggap perilaku ini tidak layak bagi perangkat infernal mereka? Haruskah saya mendahului ini dengan pernyataan if untuk memeriksa argumen?

Swizzlr
sumber
1
Saya harus mengatakan, alasan saya untuk -Semuanya adalah untuk menjadikan diri saya seorang programmer yang lebih baik. Sebagai NSHipster mengatakan : "Pro tip: Try setting the -Weverything flag and checking the “Treat Warnings as Errors” box your build settings. This turns on Hard Mode in Xcode.".
Swizzlr
2
-Weverythingbisa bermanfaat, tetapi hati-hati tentang mengubah kode Anda terlalu banyak untuk menghadapinya. Beberapa dari peringatan itu tidak hanya tidak berharga tetapi juga kontra-produktif, dan sebaiknya dimatikan. (Memang, itulah use case untuk -Weverything: mulai dengan itu, dan matikan apa yang tidak masuk akal.)
Steven Fisher
Bisakah Anda sedikit memperluas pesan peringatan untuk anak cucu? Biasanya ada lebih dari itu bagi mereka.
Steven Fisher
Setelah membaca jawabannya, saya masih lebih suka solusi awal Anda. Penggunaan pernyataan standar IMHO lebih baik daripada alternatif yang diberikan dalam jawaban. Hanya sebuah catatan kecil: jawabannya benar-benar bagus dan informatif, mereka menunjukkan cara yang keren bagaimana menyelesaikan masalah.
bbaja42
@ SevenFisher Itu adalah seluruh peringatan. Dan seperti yang ditunjukkan Killian jika saya memodifikasi enum saya nanti akan membuka kemungkinan nilai yang valid untuk diteruskan ke implementasi default. Tampaknya menjadi pola desain yang baik (jika memang seperti itu).
Swizzlr

Jawaban:

31

Berikut adalah versi yang tidak mengalami masalah yang terkait dengan pelaporan, atau versi yang Anda jaga:

void myFunction(testingMask theMask) {
    assert(theMask == MaskValueUno || theMask == MaskValueDos);
    switch (theMask) {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
    }
}

Killian telah menjelaskan mengapa dentang memancarkan peringatan: jika Anda memperpanjang enum, Anda akan jatuh ke dalam kasus default yang mungkin bukan yang Anda inginkan. Yang benar untuk dilakukan adalah menghapus case default dan mendapatkan peringatan untuk kondisi yang tidak tertangani .

Sekarang Anda khawatir seseorang dapat memanggil fungsi Anda dengan nilai di luar pencacahan. Kedengarannya seperti gagal memenuhi persyaratan fungsi: itu didokumentasikan untuk mengharapkan nilai dari testingMaskenumerasi tetapi programmer telah melewati sesuatu yang lain. Jadi buatlah kesalahan programmer menggunakan assert()(atau NSCAssert()seperti yang Anda katakan Anda menggunakan Objective-C). Buat program Anda macet dengan pesan yang menjelaskan bahwa programmer salah melakukannya, jika programmer salah.


sumber
6
+1. Sebagai modifikasi kecil, saya lebih suka memiliki masing-masing kasus return;dan menambahkan assert(false);sampai akhir (daripada mengulangi diri saya sendiri dengan mendaftar enum hukum di awal assert()dan di switch).
Josh Kelley
1
Bagaimana jika enum yang salah tidak dilewatkan oleh programmer bodoh, melainkan oleh bug kerusakan memori? Ketika pengemudi menekan istirahat Toyota mereka dan bug telah merusak enum, maka program penanganan istirahat harus crash & burn, dan harus ada teks yang ditampilkan kepada pengemudi: "programmer melakukan kesalahan, programmer buruk! ". Saya tidak begitu mengerti bagaimana ini akan membantu pengguna, sebelum mereka melewati tepi tebing.
2
@Lundin adalah apa yang dilakukan OP, atau apakah itu kasus tepi teoretis yang tidak berguna yang baru saja Anda buat? Apapun, "bug korupsi memori" [i] adalah kesalahan programmer, [ii] bukan sesuatu yang Anda dapat melanjutkan dengan cara yang bermakna (setidaknya tidak dengan persyaratan seperti yang dinyatakan dalam pertanyaan).
1
@ GrahamLee Saya hanya mengatakan bahwa "berbaring dan mati" belum tentu merupakan pilihan terbaik ketika Anda menemukan kesalahan yang tidak terduga.
1
Ini memiliki masalah baru yang Anda mungkin membiarkan pernyataan dan kasus-kasus keluar dari sinkronisasi secara tidak sengaja.
user253751
9

Memiliki defaultlabel di sini adalah indikator bahwa Anda bingung tentang apa yang Anda harapkan. Karena Anda telah kehabisan semua enumnilai yang mungkin secara eksplisit, maka nilai defaulttersebut tidak dapat dieksekusi, dan Anda tidak memerlukannya untuk menjaga terhadap perubahan di masa mendatang, karena jika Anda memperpanjang enum, maka konstruk tersebut sudah akan menghasilkan peringatan.

Jadi, kompiler memperhatikan bahwa Anda telah membahas semua pangkalan tetapi tampaknya berpikir bahwa Anda belum melakukannya, dan itu selalu merupakan pertanda buruk. Dengan mengambil upaya minimal untuk mengubah switchke bentuk yang diharapkan, Anda menunjukkan kepada kompiler bahwa apa yang tampaknya Anda lakukan adalah apa yang sebenarnya Anda lakukan, dan Anda tahu itu.

Kilian Foth
sumber
1
Tetapi kekhawatiran saya adalah variabel kotor, dan menjaga itu (seperti, seperti saya katakan, int tidak diinisialisasi). Tampaknya bagi saya bahwa dimungkinkan untuk pernyataan peralihan untuk mencapai standar dalam situasi seperti itu.
Swizzlr
Saya tidak tahu definisi untuk Objective-C dengan hati, tapi saya berasumsi bahwa typumefum akan diberlakukan oleh kompiler. Dengan kata lain, nilai "tidak diinisialisasi" hanya bisa memasukkan metode jika program Anda sudah menunjukkan perilaku tidak terdefinisi, dan saya merasa sepenuhnya masuk akal bahwa kompiler tidak memperhitungkannya.
Kilian Foth
3
@KilianFoth tidak, mereka tidak. Enum Objektif-C adalah enum C, bukan enum Jawa. Nilai apa pun dari tipe integral yang mendasari dapat hadir dalam argumen fungsi.
2
Juga, enum dapat diatur dalam runtime, dari integer. Jadi mereka dapat mengandung nilai apa pun yang bisa dibayangkan.
OP benar. testingMask m; myFunction (m); kemungkinan besar akan memukul kasus default.
Matthew James Briggs
4

Dentang bingung, memiliki pernyataan default ada praktik baik-baik saja, itu dikenal sebagai pemrograman defensif dan dianggap praktik pemrograman yang baik (1). Ini digunakan banyak dalam sistem mission-critical, meskipun mungkin tidak dalam pemrograman desktop.

Tujuan pemrograman defensif adalah untuk menangkap kesalahan tak terduga yang secara teori tidak akan pernah terjadi. Kesalahan yang tidak terduga seperti itu belum tentu programmer memberikan fungsi input yang salah, atau bahkan "peretas jahat". Kemungkinan besar, itu bisa disebabkan oleh variabel yang korup: buffer overflows, stack overflow, runaway code, dan bug serupa yang tidak terkait dengan fungsi Anda dapat menyebabkan hal ini. Dan dalam hal sistem tertanam, variabel mungkin dapat berubah karena EMI, terutama jika Anda menggunakan sirkuit RAM eksternal.

Adapun apa yang ditulis di dalam pernyataan default ... jika Anda mencurigai bahwa program tersebut telah rusak setelah Anda berakhir di sana, maka Anda memerlukan semacam penanganan kesalahan. Dalam banyak kasus Anda mungkin hanya dapat menambahkan pernyataan kosong dengan komentar: "tak terduga tetapi tidak masalah" dll, untuk menunjukkan bahwa Anda telah memikirkan situasi yang tidak mungkin.


(1) MISRA-C: 2004 15.3.


sumber
1
Tolong jangan bahwa rata-rata programmer PC menemukan konsep pemrograman defensif sepenuhnya asing, karena pandangan mereka tentang program adalah utopia abstrak di mana tidak ada yang salah dalam teori akan salah dalam praktiknya. Karena itu, Anda akan mendapatkan jawaban yang sangat beragam untuk pertanyaan Anda tergantung pada siapa yang Anda tanyakan.
3
Dentang tidak bingung; telah secara eksplisit diminta untuk memberikan semua peringatan, yang mencakup yang tidak selalu berguna, seperti peringatan gaya. (Saya pribadi memilih peringatan ini karena saya tidak ingin default di switch enum saya; memiliki mereka berarti saya tidak mendapatkan peringatan jika saya menambahkan nilai enum dan tidak menanganinya. Untuk alasan ini, saya selalu menangani nilai buruk di luar enum.)
Jens Ayton
2
@JensAyton Rasanya bingung. Semua alat analisa statis profesional serta semua kompiler yang memenuhi MISRA akan memberikan peringatan jika pernyataan switch tidak memiliki pernyataan default. Coba satu sendiri.
4
Pengkodean ke MISRA-C bukan satu-satunya penggunaan yang valid untuk kompiler C, dan, sekali lagi, -Weverythingmenghidupkan setiap peringatan, bukan pilihan yang sesuai dengan kasus penggunaan tertentu. Tidak sesuai dengan selera Anda tidak sama dengan kebingungan.
Jens Ayton
2
@Lundin: Saya cukup yakin berpegang pada MISRA-C tidak secara ajaib membuat program aman dan bebas bug ... dan saya sama-sama yakin bahwa ada banyak kode C yang aman, bebas bug dari orang-orang yang belum pernah sekalipun mendengar tentang MISRA. Bahkan, itu sedikit lebih dari pendapat seseorang (populer, tetapi tidak terbantahkan), dan ada orang yang menemukan beberapa aturannya sewenang-wenang dan kadang-kadang bahkan berbahaya di luar konteks yang mereka rancang.
cao
4

Lebih baik lagi:

typedef enum {
    MaskValueUno,
    MaskValueDos,

    MaskValue_count
} testingMask;

void myFunction(testingMask theMask) {
    assert(theMask >= 0 && theMask<MaskValue_count);
    switch theMask {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
    }
};

Ini lebih rentan kesalahan saat menambahkan item ke enum. Anda dapat melewati tes untuk> = 0 jika Anda membuat nilai enum Anda tidak ditandatangani. Metode ini hanya berfungsi jika Anda tidak memiliki celah dalam nilai enum Anda, tetapi itu sering terjadi.

Steve Weller
sumber
2
Ini mencemari jenis dengan nilai yang Anda tidak ingin jenisnya mengandung. Ini memiliki kesamaan dengan WTF tua yang baik di sini: thedailywtf.com/articles/What_Is_Truth_0x3f_
Martin Sugioarto
4

Tetapi bagaimana jika beberapa hack datang dan melempar int yang tidak diinisialisasi ke dalam fungsi saya yang cantik?

Kemudian Anda mendapatkan Perilaku Tidak Terdefinisi , dan Anda defaultakan menjadi tidak berarti. Tidak ada yang bisa Anda lakukan untuk menjadikan ini lebih baik.

Biarkan saya lebih jelas. Instan seseorang meneruskan inisialisasi intke fungsi Anda, itu adalah Perilaku Tidak Terdefinisi. Fungsi Anda dapat memecahkan Masalah Pemutusan dan tidak masalah. Itu adalah UB. Tidak ada yang dapat Anda lakukan setelah UB dipanggil.

DeadMG
sumber
3
Bagaimana kalau logging atau melempar pengecualian jika mengabaikannya diam-diam?
jgauffin
3
Memang, ada banyak yang bisa dilakukan, seperti ... menambahkan pernyataan default ke sakelar dan menangani kesalahan dengan anggun. Ini dikenal sebagai pemrograman defensif . Itu tidak hanya akan melindungi terhadap programmer bodoh menyerahkan fungsi input yang salah, tetapi juga terhadap berbagai bug yang disebabkan oleh buffer overflow, stack overflow, kode pelarian dll.
1
Tidak, itu tidak akan menangani apa pun. Itu adalah UB. Program memiliki UB saat fungsi dimasukkan, dan badan fungsi tidak bisa berbuat apa-apa.
DeadMG
2
@DeadMG Suatu enum dapat memiliki nilai apa pun yang mungkin dimiliki oleh tipe integer terkait dalam implementasi. Tidak ada yang tidak jelas tentang hal itu. Mengakses konten dari variabel otomatis tidak diinisialisasi memang UB, tetapi "peretasan jahat" (yang lebih mungkin semacam bug) mungkin juga melemparkan nilai yang terdefinisi dengan baik ke fungsi, meskipun itu bukan salah satu nilai tercantum dalam deklarasi enum.
2

Pernyataan default tidak selalu membantu. Jika sakelar lebih dari enum, nilai apa pun yang tidak didefinisikan dalam enum akan berakhir menjalankan perilaku yang tidak ditentukan.

Untuk semua yang Anda tahu, kompiler dapat mengkompilasi saklar itu (dengan default) sebagai:

if (theMask == MaskValueUno)
  // Execute something MaskValueUno code
else // theMask == MaskValueDos
  // Execute MaskValueDos code

Setelah Anda memicu perilaku yang tidak terdefinisi, tidak ada jalan untuk kembali.

Diri
sumber
2
Pertama, ini adalah nilai yang tidak ditentukan , bukan perilaku yang tidak ditentukan. Ini berarti kompiler dapat mengasumsikan beberapa nilai lain yang lebih nyaman, tetapi mungkin tidak mengubah bulan menjadi keju hijau, dll. Kedua, sejauh yang saya tahu, itu hanya berlaku untuk C ++. Dalam C, rentang nilai dari suatu enumtipe sama dengan rentang nilai dari tipe integer yang mendasarinya (yang didefinisikan oleh implementasi, karenanya harus konsisten sendiri).
Jens Ayton
1
Namun, turunan dari variabel enum dan konstanta enumerasi tidak harus memiliki lebar dan ketanda-tanganan yang sama, keduanya bersifat impl.defined. Tapi saya cukup yakin bahwa semua enum di C dievaluasi persis seperti tipe integer yang sesuai, sehingga tidak ada perilaku yang tidak ditentukan atau bahkan tidak ditentukan di sana.
2

Saya juga lebih suka memiliki default:dalam semua kasus. Saya terlambat ke pesta seperti biasa, tapi ... beberapa pemikiran lain yang tidak saya lihat di atas:

  • Peringatan khusus (atau kesalahan jika juga melempar -Werror) berasal dari -Wcovered-switch-default(dari -Weverythingtetapi tidak -Wall). Jika fleksibilitas moral Anda memungkinkan Anda mematikan peringatan tertentu (mis. Cukai beberapa hal dari -Wallatau -Weverything), pertimbangkan untuk melempar -Wno-covered-switch-default(atau -Wno-error=covered-switch-defaultketika menggunakan -Werror), dan secara umum -Wno-...untuk peringatan lain yang menurut Anda tidak menyenangkan.
  • Untuk gcc(dan perilaku yang lebih generik di clang), konsultasikan gccmanualnya untuk -Wswitch, -Wswitch-enum, -Wswitch-defaultuntuk (yang berbeda) perilaku dalam situasi yang sama dari jenis yang disebutkan dalam laporan beralih.
  • Saya juga tidak suka peringatan ini dalam konsep, dan saya tidak suka kata-katanya; bagi saya, kata-kata dari peringatan ("label default ... mencakup semua ... nilai") menunjukkan bahwa default:case akan selalu dieksekusi, seperti

    switch (foo) {
      case 1:
        do_something(); 
        //note the lack of break (etc.) here!
      default:
        do_default();
    }

    Pada pembacaan pertama, ini adalah apa yang saya pikir Anda berjalan ke - bahwa Anda default:kasus akan selalu dieksekusi karena tidak ada break;atau return;atau serupa. Konsep ini mirip (dengan telingaku) ​​dengan komentar gaya pengasuh lainnya (meskipun kadang-kadang bermanfaat) yang muncul dari clang. Jika foo == 1, kedua fungsi akan dieksekusi; kode Anda di atas memiliki perilaku ini. Yaitu, gagal break-out hanya jika Anda ingin tetap menjaga mengeksekusi kode dari kasus-kasus berikutnya! Namun, ini tampaknya bukan masalah Anda.

Dengan risiko menjadi sombong, beberapa pemikiran lain untuk kelengkapan:

  • Namun saya pikir perilaku ini (lebih) konsisten dengan memeriksa tipe agresif dalam bahasa lain atau kompiler. Jika, ketika Anda berhipotesis, beberapa reprobate memang mencoba untuk mengirimkan intatau sesuatu ke fungsi ini, yang secara eksplisit berniat untuk mengkonsumsi tipe spesifik Anda sendiri, kompiler Anda harus melindungi Anda sama baiknya dalam situasi itu dengan peringatan atau kesalahan yang agresif. TAPI tidak! (Yaitu, tampaknya paling tidak gccdan clangtidak melakukan enumpengecekan tipe, tapi saya dengar icctidak .) Karena Anda tidak mendapatkan tipe -Safety, Anda bisa mendapatkan nilai -Safety seperti dibahas di atas. Kalau tidak, seperti yang disarankan dalam TFA, pertimbangkan suatu structatau sesuatu yang dapat memberikan keamanan jenis.
  • Solusi lain dapat menciptakan "nilai" baru di Anda enumseperti MaskValueIllegal, dan tidak mendukung itu casedi Anda switch. Itu akan dimakan oleh default:(selain nilai aneh lainnya)

Pengodean bertahan lama!

hoc_age
sumber
1

Berikut adalah saran alternatif:
OP berusaha melindungi terhadap kasus di mana seseorang lewat di intmana enum diharapkan. Atau, lebih mungkin, di mana seseorang telah menautkan pustaka lama dengan program yang lebih baru menggunakan header yang lebih baru dengan lebih banyak case.

Mengapa tidak mengganti sakelar untuk menangani intkasing? Menambahkan gips di depan nilai dalam switch menghilangkan peringatan dan bahkan memberikan sedikit petunjuk tentang mengapa default ada.

void myFunction(testingMask theMask) {
    int maskValue = int(theMask);
    switch(maskValue) {
        case MaskValueUno: {} // deal with it
        case MaskValueDos: {}// deal with it
        default: {} //deal with an unexpected or uninitialized value
    }
}

Saya menemukan ini jauh lebih tidak menyenangkan daripada assert()menguji masing-masing nilai yang mungkin atau bahkan membuat asumsi bahwa kisaran nilai enum disusun dengan baik sehingga tes yang lebih sederhana bekerja. Ini hanyalah cara jelek untuk melakukan apa yang default lakukan dengan tepat dan indah.

Richard
sumber
1
posting ini agak sulit dibaca (dinding teks). Maukah Anda mengeditnya menjadi bentuk yang lebih baik?
nyamuk