Is `catch (...) {throw; } `praktik buruk?

74

Meskipun saya setuju bahwa menangkap ... tanpa rethrowing memang salah, namun saya percaya bahwa menggunakan konstruksi seperti ini:

try
{
  // Stuff
}
catch (...)
{
  // Some cleanup
  throw;
}

Dapat diterima dalam kasus di mana RAII tidak berlaku . (Tolong, jangan tanya ... tidak semua orang di perusahaan saya suka pemrograman berorientasi objek dan RAII sering dianggap sebagai "barang sekolah yang tidak berguna" ...)

Rekan kerja saya mengatakan bahwa Anda harus selalu tahu pengecualian apa yang harus dilempar dan bahwa Anda selalu dapat menggunakan konstruksi seperti:

try
{
  // Stuff
}
catch (exception_type1&)
{
  // Some cleanup
  throw;
}
catch (exception_type2&)
{
  // Some cleanup
  throw;
}
catch (exception_type3&)
{
  // Some cleanup
  throw;
}

Apakah ada praktik baik yang diakui tentang situasi ini?

sebelum
sumber
3
@Ubby: Tidak yakin ini adalah pertanyaan yang persis sama. Pertanyaan yang ditautkan adalah lebih lanjut tentang "Haruskah saya tangkap ..." sementara pertanyaan saya fokus pada "Haruskah saya menangkap ...atau <specific exception>sebelum memasang kembali"
ereOn
53
Maaf untuk mengatakan itu, tetapi C ++ tanpa RAII bukan C ++.
fredoverflow
46
Jadi pekerja sapi Anda menolak teknik yang ditemukan untuk mengatasi masalah tertentu dan kemudian berdebat tentang alternatif mana yang lebih rendah yang harus digunakan? Maaf untuk mengatakannya, tapi itu terlihat bodoh , tidak peduli ke arah mana aku melihatnya.
sbi
11
"Menangkap ... tanpa rethrowing memang salah" - Anda salah. Di main, catch(...) { return EXIT_FAILURE; }mungkin benar dalam kode yang tidak berjalan di bawah debugger. Jika Anda tidak menangkap, maka tumpukan mungkin tidak terlepas. Hanya ketika debugger Anda mendeteksi pengecualian yang tidak diinginkan, Anda ingin mereka pergi main.
Steve Jessop
3
... jadi walaupun itu adalah "kesalahan pemrograman", itu tidak selalu berarti Anda tidak ingin mengetahuinya. Bagaimanapun, kolega Anda bukan profesional perangkat lunak yang baik, jadi seperti kata sbi, sangat sulit untuk berbicara tentang cara terbaik untuk menghadapi situasi yang secara kronis lemah.
Steve Jessop

Jawaban:

196

Rekan kerja saya mengatakan bahwa Anda harus selalu tahu pengecualian apa yang harus dilempar [...]

Rekan kerja Anda, saya benci mengatakannya, jelas tidak pernah bekerja di perpustakaan untuk tujuan umum.

Bagaimana mungkin sebuah kelas seperti std::vectorbahkan berpura - pura tahu apa yang akan dibuang oleh copy constructor, sambil tetap menjamin keamanan pengecualian?

Jika Anda selalu tahu apa yang akan dilakukan callee pada waktu kompilasi, maka polimorfisme tidak akan berguna! Terkadang seluruh tujuan adalah untuk mengabstraksi apa yang terjadi di tingkat yang lebih rendah, sehingga Anda secara khusus tidak ingin tahu apa yang terjadi!

Mehrdad
sumber
32
Sebenarnya, bahkan jika mereka tahu bahwa pengecualian harus dilemparkan. Apa tujuan dari duplikasi kode ini? Kecuali penanganannya berbeda, saya tidak melihat ada gunanya menyebutkan pengecualian untuk memamerkan pengetahuan Anda.
Michael Krelin - hacker
3
@ MichaelKrelin-hacker: Itu juga. Selain itu, tambahkan fakta bahwa mereka mencabut spesifikasi pengecualian karena mendaftarkan semua pengecualian yang mungkin dalam kode cenderung menyebabkan bug di kemudian hari ... itu adalah ide terburuk yang pernah ada.
Mehrdad
4
Dan yang mengganggu saya adalah apa yang bisa menjadi asal dari sikap seperti itu ketika digabungkan dengan melihat teknik yang berguna dan nyaman sebagai "hal-hal sekolah yang tidak berguna". Tapi yah ...
Michael Krelin - hacker
1
+1, penghitungan semua opsi yang mungkin adalah resep yang sangat baik untuk kegagalan di masa depan, mengapa seseorang memilih untuk melakukannya ...?
littleadv
2
Jawaban bagus. Mungkin bisa mendapat manfaat dari menyebutkan bahwa jika kompiler yang harus didukung memiliki bug di area X, maka menggunakan fungsionalitas dari area X tidak cerdas, setidaknya untuk tidak menggunakannya secara langsung. Sebagai contoh, mengingat informasi tentang perusahaan, saya tidak akan terkejut jika mereka menggunakan Visual C ++ 6.0, yang memiliki beberapa sillybugs di daerah ini (seperti destruktor objek pengecualian dipanggil dua kali) - beberapa keturunan yang lebih kecil dari bug awal telah bertahan hingga hari ini, tetapi membutuhkan pengaturan yang cermat untuk mewujudkan.
Alf P. Steinbach
44

Apa yang tampaknya membuat Anda terperangkap adalah neraka seseorang yang mencoba memiliki kue mereka dan memakannya juga.

RAII dan pengecualian dirancang untuk berjalan beriringan. RAII adalah cara dengan mana Anda tidak memiliki untuk menulis banyak catch(...)pernyataan untuk melakukan pembersihan. Tentu saja itu akan terjadi secara otomatis. Dan pengecualian adalah satu-satunya cara untuk bekerja dengan objek RAII, karena konstruktor hanya dapat berhasil atau melempar (atau meletakkan objek dalam status kesalahan, tetapi siapa yang mau itu?).

Sebuah catchpernyataan dapat melakukan salah satu dari dua hal: menangani kesalahan atau keadaan luar biasa, atau melakukan pekerjaan pembersihan. Kadang-kadang memang keduanya, tetapi setiap catchpernyataan ada untuk melakukan setidaknya satu dari ini.

catch(...)tidak mampu melakukan penanganan pengecualian yang tepat. Anda tidak tahu apa pengecualiannya; Anda tidak dapat memperoleh informasi tentang pengecualian. Anda sama sekali tidak memiliki informasi selain fakta bahwa pengecualian dilemparkan oleh sesuatu di dalam blok kode tertentu. Satu-satunya hal yang sah yang dapat Anda lakukan di blok tersebut adalah melakukan pembersihan. Dan itu berarti melemparkan kembali pengecualian di akhir pembersihan.

Apa yang RAII berikan kepada Anda sehubungan dengan penanganan pengecualian adalah pembersihan gratis. Jika semuanya dirangkum dengan benar, maka semuanya akan dibersihkan dengan benar. Anda tidak perlu lagi memiliki catchlaporan pembersihan. Dalam hal ini, tidak ada alasan untuk menulis catch(...)pernyataan.

Jadi saya setuju bahwa catch(...)itu sebagian besar jahat ... sementara .

Ketentuan itu menjadi penggunaan RAII yang tepat. Karena tanpa itu, Anda harus dapat melakukan pembersihan tertentu. Tidak ada jalan lain; Anda harus dapat melakukan pekerjaan pembersihan. Anda harus dapat memastikan bahwa melemparkan pengecualian akan meninggalkan kode dalam keadaan yang masuk akal. Dan catch(...)merupakan alat vital dalam melakukannya.

Anda tidak dapat memiliki satu tanpa yang lain. Anda tidak bisa mengatakan bahwa keduanya RAII dan catch(...) buruk. Anda memerlukan setidaknya satu dari ini; jika tidak, Anda tidak terkecuali aman.

Tentu saja, ada satu penggunaan sah-meskipun-jarang catch(...)yang bahkan RAII tidak bisa usir: mendapatkan exception_ptruntuk meneruskan ke orang lain. Biasanya melalui promise/futureantarmuka atau serupa.

Rekan kerja saya mengatakan bahwa Anda harus selalu tahu pengecualian apa yang harus dilempar dan bahwa Anda selalu dapat menggunakan konstruksi seperti:

Rekan kerja Anda adalah seorang idiot (atau hanya sangat bodoh). Ini harus segera jelas karena berapa banyak kode salin dan rekat yang dia sarankan untuk Anda tulis. Pembersihan untuk masing-masing pernyataan tangkapan akan persis sama . Itu adalah mimpi buruk pemeliharaan, belum lagi keterbacaan.

Singkatnya: ini adalah masalah yang RAII diciptakan untuk menyelesaikannya (bukan karena itu tidak menyelesaikan masalah lain).

Yang membingungkan saya tentang gagasan ini adalah bahwa pada umumnya mundur ke bagaimana kebanyakan orang berpendapat bahwa RAII buruk. Secara umum, argumen tersebut berbunyi, "RAII buruk karena Anda harus menggunakan pengecualian untuk memberi sinyal kegagalan konstruktor. Tetapi Anda tidak dapat membuang pengecualian, karena itu tidak aman dan Anda harus memiliki banyak catchpernyataan untuk membersihkan semuanya." Yang merupakan argumen yang rusak karena RAII memecahkan masalah yang diciptakan oleh kurangnya RAII.

Kemungkinan besar, dia menentang RAII karena menyembunyikan detail. Panggilan destruktor tidak langsung terlihat pada variabel otomatis. Jadi Anda mendapatkan kode yang dipanggil secara implisit. Beberapa programmer sangat membenci itu. Rupanya, ke titik di mana mereka berpikir memiliki 3 catchpernyataan, yang semuanya melakukan hal yang sama dengan kode salin dan tempel adalah ide yang lebih baik.

Nicol Bolas
sumber
2
Sepertinya Anda tidak menulis kode yang memberikan jaminan keamanan pengecualian kuat . RAII membantu memberikan jaminan dasar . Tetapi untuk memberikan jaminan yang kuat, Anda harus membatalkan beberapa tindakan untuk mengembalikan sistem ke status yang dimilikinya sebelum fungsi dipanggil. Jaminan dasar adalah pembersihan , jaminan kuat adalah rollback . Melakukan rollback adalah fungsi khusus. Jadi Anda tidak bisa memasukkannya ke "RAII". Dan saat itulah catch-all block menjadi berguna. Jika Anda menulis kode dengan jaminan kuat, Anda menggunakan catch-all .
anton_rh
@anton_rh: Mungkin, tetapi bahkan dalam kasus-kasus itu, pernyataan catch-all adalah alat terakhir . Alat yang disukai adalah melakukan segala sesuatu yang melempar sebelum Anda mengubah keadaan apa pun yang harus Anda kembalikan dengan pengecualian. Jelas Anda tidak bisa mengimplementasikan semuanya dengan cara seperti itu dalam semua kasus, tapi itulah cara ideal untuk mendapatkan jaminan pengecualian yang kuat.
Nicol Bolas
14

Dua komentar, sungguh. Yang pertama adalah bahwa sementara di dunia yang ideal, Anda harus selalu tahu pengecualian apa yang mungkin dilemparkan, dalam praktiknya, jika Anda berurusan dengan perpustakaan pihak ketiga, atau kompilasi dengan kompiler Microsoft, Anda tidak. Lebih tepatnya,; bahkan jika Anda tahu persis semua pengecualian yang mungkin, apakah itu relevan di sini? catch (...)menyatakan niatnya jauh lebih baik daripada catch ( std::exception const& ), bahkan seandainya semua pengecualian yang mungkin berasal dari std::exception(yang akan menjadi kasus di dunia yang ideal). Adapun untuk menggunakan beberapa blok tangkap, jika tidak ada pangkalan umum untuk semua pengecualian: itu benar-benar membingungkan, dan mimpi buruk pemeliharaan. Bagaimana Anda mengenali bahwa semua perilaku itu identik? Dan itu maksudnya? Dan apa yang terjadi jika Anda harus mengubah perilaku (perbaikan bug, misalnya)? Terlalu mudah untuk melewatkannya.


sumber
3
Sebenarnya, rekan kerja saya merancang kelas pengecualiannya sendiri yang tidak berasal dari std::exceptiondan mencoba setiap hari untuk menegakkan penggunaannya di antara basis kode kami. Dugaan saya adalah ia mencoba menghukum saya karena menggunakan kode dan perpustakaan eksternal yang belum ia tulis sendiri.
sebelum
17
@ereOn Bagi saya sepertinya rekan kerja Anda sangat membutuhkan pelatihan. Bagaimanapun, saya mungkin akan menghindari menggunakan perpustakaan yang ditulis olehnya.
2
Templat dan mengetahui pengecualian apa yang akan dilempar menyatu seperti selai kacang dan tokek mati. Sesuatu yang sederhana seperti std::vector<>dapat membuang segala macam pengecualian untuk alasan apa pun.
David Thornley
3
Tolong beri tahu kami, bagaimana Anda tahu pengecualian apa yang akan diberikan oleh perbaikan bug besok di pohon panggilan?
mattnz
11

Saya pikir rekan kerja Anda telah mencampuradukkan beberapa saran yang bagus - Anda hanya harus menangani pengecualian yang diketahui dalam satu catchblok ketika Anda tidak melemparkannya kembali.

Ini berarti:

try
{
  // Stuff
}
catch (...)
{
  // General stuff
}

Apakah buruk karena diam-diam akan menyembunyikan apa pun kesalahan.

Namun:

try
{
  // Stuff
}
catch (exception_type_we_can_handle&)
{
  // Deal with the known exception
}

Tidak apa-apa - kita tahu apa yang sedang kita hadapi dan tidak perlu memaparkannya ke kode panggilan.

Juga:

try
{
  // Stuff
}
catch (...)
{
  // Rollback transactions, log errors, etc
  throw;
}

Baik-baik saja, bahkan praktik terbaik, kode untuk menangani kesalahan umum harus dengan kode yang menyebabkannya. Lebih baik daripada mengandalkan callee untuk mengetahui bahwa suatu transaksi perlu dibatalkan atau apa pun.

Keith
sumber
9

Setiap jawaban ya atau tidak harus disertai dengan alasan mengapa demikian.

Mengatakan itu salah hanya karena saya diajarkan bahwa itu hanyalah fanatisme buta.

Menulis //Some cleanup; throwbeberapa kali sama , seperti dalam contoh Anda salah karena duplikasi kode dan itu adalah beban pemeliharaan. Menulisnya sekali saja lebih baik.

Menulis catch(...)untuk membungkam semua pengecualian adalah salah karena Anda hanya harus menangani pengecualian yang Anda tahu bagaimana menangani, dan dengan wildcard itu Anda dapat memperoleh lebih dari yang Anda harapkan, dan melakukannya dapat membungkam kesalahan penting.

Tetapi jika Anda melakukan rethrow setelah a catch(...), maka alasan terakhir tidak berlaku lagi, karena Anda sebenarnya tidak menangani pengecualian, jadi tidak ada alasan mengapa ini harus dicegah.

Sebenarnya saya sudah melakukan ini untuk masuk ke fungsi sensitif tanpa masalah apa pun:

void DoSomethingImportant()
{
    try
    {
        Log("Going to do something important");
        DoIt();
    }
    catch (std::exception &e)
    {
        Log("Error doing something important: %s", e.what());
        throw;
    }
    catch (...)
    {
        Log("Unexpected error doing something important");
        throw;
    }
    Log("Success doing something important");
}
pestophagous
sumber
2
Mari berharap Log(...)tidak bisa melempar.
Deduplicator
2

Saya umumnya setuju dengan mood posting di sini, saya benar-benar tidak menyukai pola menangkap pengecualian khusus - saya pikir sintaksnya masih dalam masa pertumbuhan dan belum dapat mengatasi kode yang berlebihan.

Tetapi karena semua orang mengatakan itu, saya akan menyatu dengan fakta bahwa meskipun saya menggunakannya dengan hemat, saya sering melihat salah satu pernyataan "tangkapan (pengecualian)" saya dan berkata, "Sial, saya berharap saya sudah menelepon mengeluarkan pengecualian khusus pada saat itu "karena ketika Anda masuk nanti sering kali menyenangkan mengetahui apa maksudnya dan apa yang akan dilirik klien sekilas.

Saya tidak membenarkan sikap "Selalu gunakan x", hanya mengatakan bahwa kadang-kadang memang menyenangkan melihat mereka terdaftar dan saya yakin itu sebabnya beberapa orang berpikir itu cara "Benar" untuk melangkah.

Bill K
sumber