Mengapa spesifikasi pengecualian buruk?

50

Kembali ke sekolah sekitar 10+ tahun yang lalu, mereka mengajari Anda untuk menggunakan penentu pengecualian. Karena latar belakang saya adalah sebagai salah satu dari mereka programmer Torvaldish C yang dengan keras kepala menghindari C ++ kecuali dipaksa, saya hanya berakhir di C ++ secara sporadis, dan ketika saya melakukannya saya masih menggunakan penentu pengecualian karena itulah yang saya ajarkan.

Namun, sebagian besar programmer C ++ tampaknya tidak menyukai penentu pengecualian. Saya telah membaca debat dan argumen dari berbagai guru C ++, seperti ini . Sejauh yang saya mengerti, itu bermuara pada tiga hal:

  1. Penentu pengecualian menggunakan sistem tipe yang tidak konsisten dengan bagian bahasa lainnya ("sistem tipe bayangan").
  2. Jika fungsi Anda dengan specifier pengecualian melempar apa pun selain apa yang telah Anda tentukan, program akan dihentikan dengan cara yang buruk dan tidak terduga.
  3. Penentu pengecualian akan dihapus dalam standar C ++ yang akan datang.

Apakah saya kehilangan sesuatu di sini atau ini semua alasannya?

Pendapat saya sendiri:

Mengenai 1): Jadi apa. C ++ mungkin adalah bahasa pemrograman yang paling tidak konsisten yang pernah dibuat, sintaksis. Kami memiliki makro, goto / label, gerombolan (menimbun?) Perilaku tidak terdefinisi- / tidak ditentukan / implementasi, tipe integer yang tidak terdefinisi dengan baik, semua aturan promosi tipe implisit, kata kunci kasus khusus seperti teman, otomatis , daftar, eksplisit ... Dan seterusnya. Seseorang mungkin bisa menulis beberapa buku tebal tentang semua keanehan dalam C / C ++. Jadi mengapa orang bereaksi terhadap ketidakkonsistenan khusus ini, yang merupakan kelemahan kecil dibandingkan dengan banyak fitur lain dari bahasa yang jauh lebih berbahaya?

Mengenai 2): Bukankah itu tanggung jawab saya sendiri? Ada begitu banyak cara saya bisa menulis bug yang fatal di C ++, mengapa kasus khusus ini lebih buruk Alih-alih menulis throw(int)dan kemudian melempar Crash_t, saya mungkin juga mengklaim bahwa fungsi saya mengembalikan pointer ke int, kemudian membuat typecast liar, eksplisit dan mengembalikan pointer ke Crash_t. Semangat C / C ++ selalu meninggalkan sebagian besar tanggung jawab kepada programmer.

Lalu bagaimana dengan keuntungannya? Yang paling jelas adalah bahwa jika fungsi Anda mencoba untuk secara eksplisit melemparkan jenis apa pun selain apa yang Anda tentukan, kompiler akan memberi Anda kesalahan. Saya percaya bahwa standarnya jelas mengenai hal ini (?). Bug hanya akan terjadi ketika fungsi Anda memanggil fungsi lain yang pada gilirannya melemparkan tipe yang salah.

Berasal dari dunia yang penuh deterministik, program-program C yang tertanam, saya pasti akan lebih suka untuk mengetahui dengan tepat fungsi apa yang akan dilemparkan kepada saya. Jika ada sesuatu dalam bahasa yang mendukung itu, mengapa tidak menggunakannya? Alternatifnya tampaknya:

void func() throw(Egg_t);

dan

void func(); // This function throws an Egg_t

Saya pikir ada kemungkinan besar bahwa penelepon mengabaikan / lupa untuk menerapkan try-catch dalam kasus kedua, kurang begitu dalam kasus pertama.

Seperti yang saya pahami, jika salah satu dari kedua bentuk ini memutuskan untuk tiba-tiba melempar pengecualian lain, program akan macet. Dalam kasus pertama karena tidak diperbolehkan untuk melempar pengecualian lain, dalam kasus kedua karena tidak ada yang berharap untuk melempar SpanishInquisition_t dan oleh karena itu ekspresi itu tidak tertangkap di tempat seharusnya.

Dalam kasus yang terakhir, untuk mendapatkan tangkapan terakhir (...) pada tingkat tertinggi dari program tidak benar-benar tampak lebih baik daripada crash program: "Hei, di suatu tempat di program Anda ada sesuatu yang melempar pengecualian aneh, tidak tertangani . " Anda tidak dapat memulihkan program begitu Anda berada jauh dari tempat pengecualian itu dilemparkan, satu-satunya hal yang dapat Anda lakukan adalah keluar dari program.

Dan dari sudut pandang pengguna, mereka tidak peduli jika mereka mendapatkan kotak pesan jahat dari OS yang mengatakan "Program dihentikan. Blablabla di alamat 0x12345" atau kotak pesan jahat dari program Anda yang mengatakan "Pengecualian tidak tertangani: myclass. func.something ". Bugnya masih ada.


Dengan standar C ++ yang akan datang saya tidak akan memiliki pilihan lain selain mengabaikan penentu pengecualian. Tetapi saya lebih suka mendengar beberapa argumen yang kuat mengapa mereka buruk, daripada "Yang Mulia telah menyatakannya dan karena itu begitu". Mungkin ada lebih banyak argumen terhadap mereka daripada yang saya sebutkan, atau mungkin ada lebih banyak daripada yang saya sadari?


sumber
30
Saya tergoda untuk merendahkan ini sebagai "kata-kata kasar, menyamar sebagai pertanyaan". Anda bertanya tiga poin valid tentang E.specs, tetapi mengapa Anda mengganggu kami dengan C ++ Anda - apakah ini sangat menjengkelkan-dan-saya-suka-C-lebih baik?
Martin Ba
10
@ Martin: Karena saya ingin menunjukkan bahwa saya sama-sama bias dan tidak up to date dengan semua detail, tetapi juga bahwa saya menganggap bahasa dengan mata naif dan / atau belum hancur. Tetapi juga bahwa C dan C ++ sudah bahasa yang sangat cacat, jadi satu cacat lebih atau kurang tidak terlalu penting. Posting itu sebenarnya jauh lebih buruk sebelum saya mengeditnya :) Argumen terhadap penentu pengecualian juga cukup subyektif dan karena itu sulit untuk membahasnya tanpa mendapatkan subjektif sendiri.
SpanishInquisition_t! Lucu sekali! Saya pribadi terkesan dengan penggunaan stack frame pointer untuk melempar pengecualian dan sepertinya itu bisa membuat kode jauh lebih bersih. Namun, saya tidak pernah benar-benar menulis kode dengan pengecualian. Sebut saya kuno, tetapi nilai-nilai pengembalian berfungsi dengan baik untuk saya.
Shahbaz
@ Shahbaz Seperti yang bisa dibaca orang, saya agak kuno juga, tapi tetap saja saya tidak pernah benar-benar mempertanyakan apakah pengecualian sendiri baik atau buruk.
2
Bentuk lampau dari lemparan adalah melemparkan , tidak throwed . Ini kata kerja yang kuat.
TRiG

Jawaban:

48

Spesifikasi pengecualian buruk karena mereka ditegakkan dengan lemah, dan karena itu tidak benar-benar menghasilkan banyak, dan mereka juga buruk karena memaksa run-time untuk memeriksa pengecualian yang tidak terduga sehingga mereka dapat berakhir (), bukannya meminta UB , ini dapat membuang banyak kinerja.

Jadi, secara ringkas, spesifikasi pengecualian tidak ditegakkan cukup kuat dalam bahasa untuk benar-benar membuat kode lebih aman, dan menerapkannya seperti yang ditentukan adalah menguras kinerja besar.

DeadMG
sumber
2
Tetapi pemeriksaan run-time bukanlah kesalahan dari spec pengecualian seperti itu, tetapi lebih dari bagian standar apa pun yang menyatakan bahwa harus ada terminasi jika jenis yang salah dilemparkan. Bukankah lebih pintar untuk menghapus persyaratan yang mengarah ke pemeriksaan dinamis ini dan membiarkan semuanya dievaluasi secara statis oleh kompiler? Sepertinya RTTI adalah penyebab sebenarnya di sini, atau ...?
7
@Lundin: tidak mungkin untuk secara statis menentukan semua pengecualian yang dapat dilempar dari fungsi dalam C ++, karena bahasa memungkinkan perubahan sewenang-wenang dan non-deterministik pada aliran kontrol saat runtime (misalnya melalui pointer fungsi, metode virtual dan sejenisnya). Menerapkan pengecekan waktu kompilasi statis seperti Java akan membutuhkan perubahan mendasar pada bahasa dan banyak kompatibilitas-C yang harus dinonaktifkan.
3
@ OrbWeaver: Saya pikir pemeriksaan statis sangat mungkin - spec pengecualian akan menjadi bagian dari spesifikasi fungsi (seperti nilai balik), dan pointer fungsi, virtual override, dll. Harus memiliki spesifikasi yang kompatibel. Keberatan utama adalah bahwa itu fitur semua atau tidak sama sekali - fungsi dengan spesifikasi pengecualian statis tidak dapat memanggil fungsi lawas. (Memanggil fungsi C akan baik-baik saja, karena mereka tidak dapat membuang apa pun).
Mike Seymour
2
@Lundin: Spesifikasi pengecualian belum dihapus, hanya ditinggalkan. Kode lawas yang menggunakannya masih akan valid.
Mike Seymour
1
@Lundin: Versi lama C ++ dengan spesifikasi pengecualian sangat kompatibel. Mereka tidak akan gagal untuk mengkompilasi atau menjalankan secara tidak terduga jika kode itu valid sebelumnya.
DeadMG
18

Salah satu alasan tidak ada yang menggunakannya adalah karena asumsi utama Anda salah:

"[Keuntungan] yang paling jelas adalah bahwa jika fungsi Anda mencoba untuk secara eksplisit membuang jenis apa pun selain apa yang Anda tentukan, kompiler akan memberi Anda kesalahan."

struct foo {};
struct bar {};

struct test
{
    void baz() throw(foo)
    {
        throw bar();
    }
};

int main()
{
    test x;
    try { x.baz(); } catch(bar &b) {}
}

Program ini dikompilasi tanpa kesalahan atau peringatan .

Selain itu, meskipun pengecualian akan ditangkap , program masih berakhir.


Sunting: untuk menjawab suatu titik dalam pertanyaan Anda, tangkap (...) (atau lebih baik, tangkap (std :: pengecualian &) atau kelas dasar lainnya, dan kemudian tangkap (...)) masih berguna bahkan jika Anda tidak tahu persis apa yang salah.

Pertimbangkan bahwa pengguna Anda telah menekan tombol menu untuk "save". Pawang untuk tombol menu mengundang aplikasi untuk menyimpan. Ini bisa gagal karena berbagai alasan: file itu pada sumber daya jaringan yang lenyap, ada file read-only dan tidak dapat disimpan, dll. Tapi pawang tidak benar-benar peduli mengapa sesuatu gagal; hanya peduli apakah itu berhasil atau gagal. Jika berhasil, semuanya baik. Jika gagal, itu dapat memberi tahu pengguna. Sifat eksepsi yang tepat tidak relevan. Selain itu, jika Anda menulis kode yang benar, pengecualian-aman, itu berarti bahwa kesalahan semacam itu dapat menyebar melalui sistem Anda tanpa menjatuhkannya - bahkan untuk kode yang tidak peduli dengan kesalahan tersebut. Ini membuatnya lebih mudah untuk memperluas sistem. Misalnya, Anda sekarang menyimpan melalui koneksi database.

Kaz Dragon
sumber
4
@Lundin dikompilasi tanpa peringatan. Dan tidak, kamu tidak bisa. Tidak bisa dipercaya. Anda dapat memanggil melalui pointer fungsi atau itu bisa menjadi fungsi anggota virtual, dan kelas turunan melempar turunanex_pengertian (yang tidak mungkin diketahui oleh kelas dasar).
Kaz Dragon
Keberuntungan yang cukup. Embarcadero / Borland memberikan peringatan untuk ini: "melempar pengecualian melanggar specifier pengecualian". Rupanya GCC tidak. Tidak ada alasan mereka tidak bisa menerapkan peringatan. Demikian pula, alat analisis statis dapat dengan mudah menemukan bug ini.
14
@Lundin: Tolong jangan mulai berdebat dan mengulang informasi palsu. Pertanyaan Anda sudah menjadi kata-kata kasar, dan mengklaim hal-hal palsu tidak membantu. Alat analisis statis TIDAK BISA menemukan jika ini terjadi; untuk melakukannya akan melibatkan pemecahan Masalah Pemutusan. Selain itu, perlu menganalisis keseluruhan program, dan sangat sering seluruh sumber tidak tersedia.
David Thornley
1
@Lundin alat analisis statis yang sama bisa memberi tahu Anda yang dilempar pengecualian meninggalkan tumpukan Anda, jadi dalam hal ini menggunakan spesifikasi pengecualian masih tidak memberi Anda apa pun kecuali potensi negatif-negatif yang akan menjatuhkan program Anda di mana Anda bisa menangani kasus kesalahan di cara yang jauh lebih baik (seperti mengumumkan kegagalan, dan terus berjalan: lihat bagian kedua jawaban saya sebagai contoh).
Kaz Dragon
1
@Lundin Sebuah pertanyaan yang bagus. Jawabannya adalah, secara umum, Anda tidak tahu apakah fungsi-fungsi yang Anda panggil akan mengeluarkan pengecualian, jadi anggapan yang aman adalah bahwa mereka semua melakukannya. Di mana untuk menangkap ini adalah pertanyaan yang saya jawab di stackoverflow.com/questions/4025667/… . Penangan acara dalam bentuk tertentu dari batas modul alami. Mengizinkan pengecualian untuk melarikan diri ke sistem jendela / peristiwa umum (mis.), Akan dihukum. Pawang harus menangkap mereka semua jika program ingin bertahan di sana.
Kaz Dragon
17

Wawancara dengan Anders Hejlsberg ini cukup terkenal. Di dalamnya, ia menjelaskan mengapa tim desain C # membuang pengecualian yang diperiksa. Singkatnya, ada dua alasan utama: kemampuan versi dan skalabilitas.

Saya tahu OP berfokus pada C ++ sedangkan Hejlsberg membahas C #, tetapi poin yang Hejlsberg buat juga sangat cocok untuk C ++.

CesarGon
sumber
5
"Poin yang Hejlsberg buat juga sangat cocok untuk C ++" .. Salah! Pengecualian yang diperiksa seperti yang diterapkan di Jawa memaksa penelepon untuk menanganinya (dan / atau penelepon, dll.). Spesifikasi Exception di C ++ (sementara cukup lemah dan tidak berguna) hanya berlaku untuk satu "fungsi-call-batas" dan jangan tidak "menyebarkan panggilan stack" seperti pengecualian diperiksa lakukan.
Martin Ba
3
@ Martin: Saya setuju dengan Anda tentang perilaku spesifikasi pengecualian di C ++. Tapi frasa saya yang Anda kutip "poin yang Hejlsberg buat juga sangat cocok untuk C ++" mengacu, yah, poin yang Hejlsberg buat, daripada spesifikasi C ++. Dengan kata lain, saya berbicara di sini tentang kemampuan versi dan skalabilitas program, daripada propagasi pengecualian.
CesarGon
3
Saya tidak setuju dengan Hejlsberg cukup awal: "Kekhawatiran saya tentang pengecualian diperiksa adalah borgol yang mereka pasang pada programmer. Anda melihat programmer mengambil API baru yang memiliki semua klausa pelemparan ini, dan kemudian Anda melihat bagaimana kode mereka berbelit-belit, dan Anda menyadari pengecualian yang dicentang tidak membantu mereka. Ini semacam desainer API diktator yang memberi tahu Anda cara melakukan penanganan pengecualian. " --- Pengecualian diperiksa atau tidak, Anda masih harus menangani pengecualian yang dilemparkan oleh API yang Anda panggil. Pengecualian yang dicentang hanya membuatnya menjadi eksplisit.
quant_dev
5
@quant_dev: Tidak, Anda tidak harus menangani pengecualian yang dilemparkan oleh API yang Anda panggil. Pilihan yang benar-benar valid adalah tidak menangani pengecualian dan membiarkannya menggelembungkan tumpukan panggilan untuk ditangani oleh penelepon Anda.
CesarGon
4
@CesarGon Tidak menangani pengecualian juga membuat pilihan bagaimana menanganinya.
quant_dev