Penanganan pengecualian (EH) tampaknya menjadi standar saat ini, dan dengan mencari di web, saya tidak dapat menemukan ide atau metode baru yang mencoba meningkatkan atau menggantinya (well, beberapa variasi ada, tetapi tidak ada yang baru).
Meskipun kebanyakan orang tampaknya mengabaikannya atau hanya menerimanya, EH memiliki beberapa kelemahan besar: pengecualian tidak terlihat oleh kode dan itu menciptakan banyak, banyak kemungkinan titik keluar. Joel pada perangkat lunak menulis artikel tentang itu . Perbandingannya sangat goto
pas, itu membuat saya berpikir lagi tentang EH.
Saya mencoba menghindari EH dan hanya menggunakan nilai balik, panggilan balik, atau apa pun yang sesuai dengan tujuannya. Tetapi ketika Anda harus menulis kode yang dapat diandalkan, Anda tidak dapat mengabaikan EH hari ini : Dimulai dengan new
, yang dapat membuang pengecualian, bukan hanya mengembalikan 0 (seperti di masa lalu). Ini membuat setiap baris kode C ++ rentan terhadap pengecualian. Dan kemudian lebih banyak tempat di C ++ pengecualian kode dasar melemparkan ... std lib melakukannya, dan seterusnya.
Ini terasa seperti berjalan di tanah yang goyah .. Jadi, sekarang kita dipaksa untuk memperhatikan pengecualian!
Tapi ini sulit, sangat sulit. Anda harus belajar menulis kode aman pengecualian, dan bahkan jika Anda memiliki pengalaman dengannya, masih harus memeriksa ulang setiap baris kode untuk aman! Atau Anda mulai meletakkan blok coba / tangkap di mana-mana, yang mengacaukan kode hingga mencapai kondisi tidak terbaca.
EH menggantikan pendekatan deterministik bersih yang lama (nilai pengembalian ..), yang memiliki beberapa kelemahan tetapi dapat dipahami dan mudah dipecahkan dengan pendekatan yang menciptakan banyak kemungkinan titik keluar dalam kode Anda, dan jika Anda mulai menulis kode yang menangkap pengecualian (apa yang Anda lakukan) terpaksa dilakukan di beberapa titik), maka itu bahkan menciptakan banyak jalur melalui kode Anda (kode di blok tangkap, pikirkan tentang program server di mana Anda memerlukan fasilitas logging selain std :: cerr ..). EH memiliki kelebihan, tapi bukan itu intinya.
Pertanyaan aktual saya:
- Apakah Anda benar-benar menulis kode aman pengecualian?
- Apakah Anda yakin kode "siap produksi" terakhir Anda merupakan pengecualian aman?
- Bisakah Anda yakin, apakah itu?
- Apakah Anda tahu dan / atau benar-benar menggunakan alternatif yang berfungsi?
sumber
Jawaban:
Pertanyaan Anda membuat pernyataan, bahwa "Menulis kode pengecualian-aman sangat sulit". Saya akan menjawab pertanyaan Anda terlebih dahulu, dan kemudian, menjawab pertanyaan tersembunyi di baliknya.
Menjawab pertanyaan
Tentu saja saya lakukan.
Ini adalah yang alasan Java kehilangan banyak daya tariknya kepada saya sebagai C ++ programmer (kurangnya semantik RAII), tapi saya melantur: Ini adalah C ++ pertanyaan.
Faktanya, ini diperlukan ketika Anda harus bekerja dengan STL atau kode Boost. Misalnya, utas C ++ (
boost::thread
ataustd::thread
) akan mengeluarkan pengecualian untuk keluar dengan anggun.Menulis kode pengecualian-aman seperti menulis kode bebas bug.
Anda tidak dapat 100% yakin kode Anda pengecualian aman. Tapi kemudian, Anda berjuang untuk itu, menggunakan pola-pola terkenal, dan menghindari pola-anti terkenal.
Tidak ada alternatif yang layak di C ++ (yaitu Anda harus kembali ke C dan menghindari perpustakaan C ++, serta kejutan eksternal seperti Windows SEH).
Menulis pengecualian kode aman
Untuk menulis kode aman pengecualian, Anda harus tahu dulu tingkat keamanan pengecualian apa saja setiap instruksi yang Anda tulis.
Misalnya, a
new
bisa melempar pengecualian, tetapi menetapkan bawaan (misalnya int, atau pointer) tidak akan gagal. Suatu swap tidak akan pernah gagal (jangan pernah menulis swap melempar), sebuahstd::list::push_back
...Jaminan pengecualian
Hal pertama yang harus dipahami adalah bahwa Anda harus dapat mengevaluasi jaminan pengecualian yang ditawarkan oleh semua fungsi Anda:
Contoh kode
Kode berikut ini sepertinya benar C ++, tetapi sebenarnya, menawarkan jaminan "tidak ada", dan dengan demikian, itu tidak benar:
Saya menulis semua kode saya dengan analisis semacam ini.
Jaminan terendah yang ditawarkan adalah dasar, tetapi kemudian, pemesanan setiap instruksi membuat seluruh fungsi "tidak ada", karena jika 3. melempar, x akan bocor.
Hal pertama yang harus dilakukan adalah membuat fungsi "dasar", yaitu meletakkan x di penunjuk pintar hingga aman dimiliki oleh daftar:
Sekarang, kode kami menawarkan jaminan "dasar". Tidak ada yang akan bocor, dan semua benda akan berada dalam kondisi yang benar. Tapi kami bisa menawarkan lebih banyak, yaitu jaminan kuat. Di sinilah bisa menjadi mahal, dan inilah sebabnya tidak semua kode C ++ kuat. Mari kita coba:
Kami memesan ulang operasi, pertama-tama membuat dan mengatur
X
ke nilai yang tepat. Jika operasi apa pun gagal, makat
tidak dimodifikasi, jadi, operasi 1 hingga 3 dapat dianggap "kuat": Jika sesuatu melempar,t
tidak dimodifikasi, danX
tidak akan bocor karena dimiliki oleh smart pointer.Kemudian, kami membuat copy
t2
darit
, dan bekerja pada copy ini dari operasi 4 sampai 7. Jika sesuatu melempar,t2
dimodifikasi, tapi kemudian,t
masih asli. Kami masih menawarkan jaminan yang kuat.Kemudian, kami bertukar
t
dant2
. Operasi swap harus nothrow dalam C ++, jadi mari kita berharap swap yang Anda tulisT
adalah nothrow (jika tidak, tulis ulang sehingga tidakhrow).Jadi, jika kita mencapai akhir fungsi, semuanya berhasil (Tidak perlu tipe pengembalian) dan
t
memiliki nilai yang dikecualikan. Jika gagal, makat
masih memiliki nilai aslinya.Sekarang, menawarkan jaminan yang kuat bisa sangat mahal, jadi jangan berusaha untuk menawarkan jaminan yang kuat untuk semua kode Anda, tetapi jika Anda dapat melakukannya tanpa biaya (dan C ++ inlining dan pengoptimalan lainnya dapat membuat semua kode di atas tanpa biaya) , maka lakukanlah. Pengguna fungsi akan berterima kasih untuk itu.
Kesimpulan
Dibutuhkan kebiasaan untuk menulis kode pengecualian-aman. Anda harus mengevaluasi jaminan yang ditawarkan oleh setiap instruksi yang akan Anda gunakan, dan kemudian, Anda harus mengevaluasi jaminan yang ditawarkan oleh daftar instruksi.
Tentu saja, kompiler C ++ tidak akan mencadangkan jaminan (dalam kode saya, saya menawarkan jaminan sebagai tag peringatan oksigen @), yang agak menyedihkan, tetapi seharusnya tidak menghentikan Anda dari mencoba menulis kode pengecualian-aman.
Kegagalan normal vs. bug
Bagaimana seorang programmer menjamin bahwa fungsi yang tidak gagal akan selalu berhasil? Lagi pula, fungsinya bisa memiliki bug.
Ini benar. Jaminan pengecualian seharusnya ditawarkan oleh kode bebas bug. Tetapi kemudian, dalam bahasa apa pun, memanggil fungsi mengandaikan fungsi tersebut bebas bug. Tidak ada kode waras yang melindungi dirinya dari kemungkinan ada bug. Tulis kode sebaik mungkin, lalu tawarkan jaminan dengan anggapan itu bebas bug. Dan jika ada bug, perbaiki.
Pengecualian untuk kegagalan pemrosesan yang luar biasa, bukan untuk bug kode.
Kata-kata terakhir
Sekarang, pertanyaannya adalah "Apakah ini sepadan?".
Tentu saja. Memiliki fungsi "nothrow / no-fail" mengetahui bahwa fungsi tidak akan gagal adalah keuntungan besar. Hal yang sama dapat dikatakan untuk fungsi "kuat", yang memungkinkan Anda untuk menulis kode dengan semantik transaksional, seperti basis data, dengan fitur komit / kembalikan, komit merupakan eksekusi normal dari kode, melemparkan pengecualian sebagai kembalikan.
Kemudian, "dasar" adalah jaminan yang paling tidak Anda tawarkan. C ++ adalah bahasa yang sangat kuat di sana, dengan cakupannya, memungkinkan Anda untuk menghindari kebocoran sumber daya (sesuatu yang sulit ditawarkan oleh pengumpul sampah untuk basis data, koneksi, atau pegangan file).
Jadi, sejauh yang saya lihat, itu sangat berharga.
Edit 2010-01-29: Tentang swap non-lempar
nobar membuat komentar yang saya percaya, cukup relevan, karena itu adalah bagian dari "bagaimana Anda menulis pengecualian kode aman":
swap()
fungsi yang ditulis khusus . Namun perlu dicatat bahwa hal itustd::swap()
dapat gagal berdasarkan operasi yang digunakan secara internaldefault
std::swap
akan membuat salinan dan tugas, yang, untuk beberapa objek, bisa melempar. Dengan demikian, swap default dapat dilempar, baik digunakan untuk kelas Anda atau bahkan untuk kelas STL. Sejauh menyangkut standar C ++, operasi swap untukvector
,,deque
danlist
tidak akan melempar, sedangkan itu bisa karenamap
jika fungsi perbandingan dapat membuang konstruksi salinan (Lihat Bahasa Pemrograman C ++, Edisi Khusus, lampiran E, E.4.3 .Swap ).Melihat implementasi Visual C ++ 2008 dari swap vektor, swap vektor tidak akan membuang jika dua vektor memiliki pengalokasi yang sama (yaitu, kasus normal), tetapi akan membuat salinan jika mereka memiliki pengalokasi yang berbeda. Dan dengan demikian, saya berasumsi bisa melempar dalam kasus terakhir ini.
Jadi, teks asli masih berlaku: Jangan pernah menulis swap lempar, tetapi komentar nobar harus diingat: Pastikan objek yang Anda bertukar memiliki swap non-lempar.
Edit 2011-11-06: Artikel menarik
Dave Abrahams , yang memberi kami jaminan dasar / kuat / tidak lincah , menjelaskan dalam sebuah artikel pengalamannya tentang membuat pengecualian STL aman:
http://www.boost.org/community/exception_safety.html
Lihatlah poin 7 (Pengujian otomatis untuk keselamatan pengecualian), di mana ia bergantung pada pengujian unit otomatis untuk memastikan setiap kasus diuji. Saya kira bagian ini adalah jawaban yang sangat baik untuk pertanyaan penulis, " Bisakah Anda yakin, apakah itu? ".
Sunting 2013-05-31: Komentar dari dionadar
Dionadar mengacu pada baris berikut, yang memang memiliki perilaku tidak terdefinisi.
Solusi di sini adalah untuk memverifikasi apakah integer sudah pada nilai maksimalnya (menggunakan
std::numeric_limits<T>::max()
) sebelum melakukan penambahan.Kesalahan saya akan muncul di bagian "Normal failure vs. bug", yaitu bug. Itu tidak membatalkan alasan, dan itu tidak berarti kode pengecualian-aman tidak berguna karena tidak mungkin untuk dicapai. Anda tidak dapat melindungi diri Anda dari komputer yang dimatikan, atau bug penyusun, atau bahkan bug Anda, atau kesalahan lainnya. Anda tidak dapat mencapai kesempurnaan, tetapi Anda bisa berusaha sedekat mungkin.
Saya memperbaiki kode dengan komentar Dionadar.
sumber
"finally" does exactly what you were trying to achieve
Tentu saja. Dan karena mudah untuk menghasilkan kode rapuh denganfinally
, gagasan "coba-dengan-sumber daya" akhirnya diperkenalkan di Jawa 7 (yaitu, 10 tahun setelah C #using
dan 3 dekade setelah C ++ destructor). Inilah kerapuhan yang saya kritik. AdapunJust because [finally] doesn't match your taste (RAII doesn't match mine, [...]) doesn't mean it's "failing"
: Dalam hal ini, industri tidak setuju dengan selera Anda, karena bahasa yang Dikumpulkan Sampah cenderung menambahkan pernyataan yang diilhami RAII (bahasa C #using
dan Javatry
).RAII doesn't match mine, since it needs a new struct every single darn time, which is tedious sometimes
Tidak, tidak. Anda dapat menggunakan pointer pintar atau kelas utilitas untuk "melindungi" sumber daya.Menulis kode pengecualian-aman di C ++ tidak banyak tentang menggunakan banyak blok coba {} catch {}. Ini tentang mendokumentasikan jenis jaminan apa yang diberikan kode Anda.
Saya merekomendasikan untuk membaca seri Ramuan Guru dari Seminggu , khususnya angsuran 59, 60 dan 61.
Untuk meringkas, ada tiga tingkat keamanan pengecualian yang dapat Anda berikan:
Secara pribadi, saya menemukan artikel ini agak terlambat, begitu banyak kode C ++ saya jelas bukan pengecualian aman.
sumber
Beberapa dari kita telah menggunakan pengecualian selama lebih dari 20 tahun. PL / Saya memilikinya, misalnya. Premis bahwa mereka adalah teknologi baru dan berbahaya tampaknya dipertanyakan oleh saya.
sumber
Pertama-tama (seperti yang dinyatakan Neil), SEH adalah Penanganan Pengecualian Terstruktur dari Microsoft. Ini mirip dengan tetapi tidak identik dengan pemrosesan pengecualian dalam C ++. Bahkan, Anda harus mengaktifkan Penanganan Pengecualian C ++ jika Anda menginginkannya di Visual Studio - perilaku default tidak menjamin bahwa objek lokal dihancurkan dalam semua kasus! Dalam kedua kasus tersebut, Penanganan Pengecualian tidak terlalu sulit melainkan hanya berbeda .
Sekarang untuk pertanyaan Anda yang sebenarnya.
Iya. Saya berusaha keras untuk pengecualian kode aman dalam semua kasus. Saya menginjili menggunakan teknik RAII untuk akses lingkup ke sumber daya (misalnya,
boost::shared_ptr
untuk memori,boost::lock_guard
untuk mengunci). Secara umum, penggunaan RAII dan teknik pelindung lingkup yang konsisten akan membuat kode aman pengecualian lebih mudah untuk ditulis. Caranya adalah dengan mempelajari apa yang ada dan bagaimana menerapkannya.Tidak. Ini sama amannya dengan itu. Saya dapat mengatakan bahwa saya belum melihat kesalahan proses karena pengecualian dalam beberapa tahun kegiatan 24/7. Saya tidak mengharapkan kode yang sempurna, hanya kode yang ditulis dengan baik. Selain memberikan keamanan pengecualian, teknik di atas menjamin kebenaran dengan cara yang hampir mustahil untuk dicapai dengan
try
/catch
blok. Jika Anda menangkap semua yang ada di lingkup kendali teratas Anda (utas, proses, dll.), Maka Anda dapat yakin bahwa Anda akan terus berjalan di hadapan pengecualian ( sebagian besar waktu ). Teknik yang sama juga akan membantu Anda terus berjalan dengan benar dalam menghadapi pengecualian tanpatry
/catch
memblokir di mana-mana .Iya. Anda dapat yakin dengan audit kode menyeluruh tetapi tidak ada yang benar-benar melakukannya? Ulasan kode reguler dan pengembang yang berhati-hati akan pergi ke sana.
Saya telah mencoba beberapa variasi selama bertahun-tahun seperti negara pengkodean di bit atas (ala
HRESULT
s ) atausetjmp() ... longjmp()
hack mengerikan . Keduanya terurai dalam praktik meskipun dengan cara yang sangat berbeda.Pada akhirnya, jika Anda terbiasa menerapkan beberapa teknik dan dengan hati-hati memikirkan di mana Anda benar-benar dapat melakukan sesuatu sebagai tanggapan terhadap pengecualian, Anda akan berakhir dengan kode yang sangat mudah dibaca yang merupakan pengecualian aman. Anda dapat menyimpulkan ini dengan mengikuti aturan-aturan ini:
try
/catch
kapan Anda dapat melakukan sesuatu tentang pengecualian tertentunew
ataudelete
dalamstd::sprintf
,,snprintf
dan array secara umum - gunakanstd::ostringstream
untuk memformat dan mengganti array denganstd::vector
danstd::string
Saya hanya dapat merekomendasikan agar Anda mempelajari cara menggunakan pengecualian dengan benar dan melupakan kode hasil jika Anda berencana untuk menulis dalam C ++. Jika Anda ingin menghindari pengecualian, Anda mungkin ingin mempertimbangkan untuk menulis dalam bahasa lain yang tidak memilikinya atau membuatnya aman . Jika Anda ingin benar-benar belajar bagaimana memanfaatkan sepenuhnya C ++, bacalah beberapa buku dari Herb Sutter , Nicolai Josuttis , dan Scott Meyers .
sumber
new
ataudelete
dalam": by raw Saya kira maksud Anda di luar konstruktor atau destruktor.delete
tidak boleh digunakan di luar implementasitr1::shared_ptr
dan sejenisnya.new
dapat digunakan asalkan penggunaannya sepertitr1::shared_ptr<X> ptr(new X(arg, arg));
. Bagian penting adalah bahwa hasilnew
langsung masuk ke pointer dikelola. The halaman diboost::shared_ptr
Best Practices menggambarkan yang terbaik.Tidak mungkin untuk menulis kode pengecualian-aman di bawah asumsi bahwa "baris apa pun bisa melempar". Desain kode pengecualian-aman bergantung secara kritis pada kontrak / jaminan tertentu yang Anda harapkan, patuhi, ikuti, dan terapkan dalam kode Anda. Sangat penting untuk memiliki kode yang dijamin tidak akan pernah dibuang. Ada beberapa jenis pengecualian lain di luar sana.
Dengan kata lain, membuat kode pengecualian-aman sebagian besar merupakan masalah desain program, bukan hanya masalah pengkodean biasa .
sumber
Yah, tentu saja saya bermaksud.
Saya yakin bahwa server 24/7 saya dibuat menggunakan pengecualian yang dijalankan 24/7 dan tidak bocor memori.
Sangat sulit untuk memastikan bahwa kode apa pun benar. Biasanya, seseorang hanya bisa mengikuti hasil
Tidak. Menggunakan pengecualian lebih bersih dan lebih mudah daripada alternatif yang saya gunakan selama 30 tahun terakhir dalam pemrograman.
sumber
Mengesampingkan kebingungan antara pengecualian SEH dan C ++, Anda harus menyadari bahwa pengecualian dapat dilemparkan kapan saja, dan menulis kode Anda dengan itu dalam pikiran. Kebutuhan akan keamanan pengecualian sebagian besar mendorong penggunaan RAII, smart pointer, dan teknik C ++ modern lainnya.
Jika Anda mengikuti pola yang sudah ada, menulis kode pengecualian-aman tidak terlalu sulit, dan sebenarnya lebih mudah daripada menulis kode yang menangani pengembalian kesalahan dengan benar dalam semua kasus.
sumber
EH baik, secara umum. Tetapi implementasi C ++ tidak terlalu ramah karena sangat sulit untuk mengatakan seberapa baik pengecualian Anda menangkap cakupan. Java misalnya membuat ini mudah, kompiler akan cenderung gagal jika Anda tidak menangani kemungkinan pengecualian.
sumber
noexcept
.Saya sangat suka bekerja dengan Eclipse dan Java (baru ke Java), karena ia melempar kesalahan pada editor jika Anda kehilangan EH handler. Itu membuat banyak hal lebih sulit untuk dilupakan untuk menangani pengecualian ...
Plus, dengan alat IDE, itu menambah blok coba / tangkap atau blok tangkap lain secara otomatis.
sumber
Beberapa dari kita lebih suka bahasa seperti Java yang memaksa kita untuk mendeklarasikan semua pengecualian yang dilemparkan dengan metode, daripada membuatnya tidak terlihat seperti dalam C ++ dan C #.
Bila dilakukan dengan benar, pengecualian lebih baik daripada kode pengembalian kesalahan, jika karena alasan lain Anda tidak perlu menyebarkan kegagalan pada rantai panggilan secara manual.
Yang sedang berkata, pemrograman perpustakaan API tingkat rendah mungkin harus menghindari penanganan pengecualian, dan tetap berpegang pada kode pengembalian kesalahan.
Sudah pengalaman saya bahwa sulit untuk menulis kode penanganan pengecualian bersih dalam C ++. Saya akhirnya menggunakan
new(nothrow)
banyak.sumber
new(std::nothrow)
tidak cukup. Omong-omong, lebih mudah untuk menulis kode pengecualian-aman di C ++ daripada di Jawa: en.wikipedia.org/wiki/Resource_Acquisition_Is_InitializationSaya mencoba yang terbaik untuk menulis kode pengecualian-aman, ya.
Itu berarti saya berhati-hati untuk mengawasi garis mana yang bisa dilempar. Tidak semua orang bisa, dan sangat penting untuk mengingatnya. Kuncinya adalah benar-benar memikirkan, dan merancang kode Anda untuk memenuhi, pengecualian pengecualian yang ditentukan dalam standar.
Bisakah operasi ini ditulis untuk memberikan jaminan pengecualian yang kuat? Apakah saya harus puas dengan yang mendasar? Baris mana yang bisa mengeluarkan pengecualian, dan bagaimana saya bisa memastikan bahwa jika mereka melakukannya, mereka tidak merusak objek?
sumber
Apakah Anda benar-benar menulis kode aman pengecualian? [Tidak ada hal seperti itu. Pengecualian adalah pelindung kertas dari kesalahan kecuali jika Anda memiliki lingkungan terkelola. Ini berlaku untuk tiga pertanyaan pertama.]
Apakah Anda tahu dan / atau benar-benar menggunakan alternatif yang berfungsi? [Alternatif untuk apa? Masalahnya di sini adalah orang tidak memisahkan kesalahan aktual dari operasi program normal. Jika operasi program normal (yaitu file tidak ditemukan), itu bukan penanganan kesalahan. Jika itu adalah kesalahan aktual, tidak ada cara untuk 'menanganinya' atau itu bukan kesalahan sebenarnya. Tujuan Anda di sini adalah untuk mengetahui apa yang salah dan menghentikan spreadsheet dan mencatat kesalahan, memulai kembali driver ke pemanggang Anda, atau hanya berdoa agar jetfighter dapat terus terbang bahkan ketika perangkat lunaknya bermasalah dan berharap yang terbaik.]
sumber
Banyak (bahkan saya akan mengatakan sebagian besar) orang lakukan.
Yang benar-benar penting tentang pengecualian, adalah jika Anda tidak menulis kode penanganan apa pun - hasilnya benar-benar aman dan berperilaku baik. Terlalu ingin panik, tetapi aman.
Anda perlu secara aktif membuat kesalahan dalam penangan untuk mendapatkan sesuatu yang tidak aman, dan hanya menangkap (...) {} akan membandingkan dengan mengabaikan kode kesalahan.
sumber
f = new foo(); f->doSomething(); delete f;
Jika metode doSomething melempar pengecualian, maka Anda memiliki kebocoran memori.