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
Apakah Anda benar-benar menulis kode aman pengecualian?
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
atau std::thread
) akan mengeluarkan pengecualian untuk keluar dengan anggun.
Apakah Anda yakin kode "siap produksi" terakhir Anda merupakan pengecualian aman?
Bisakah Anda yakin, apakah itu?
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.
Apakah Anda tahu dan / atau benar-benar menggunakan alternatif yang berfungsi?
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), sebuah std::list::push_back
...
Jaminan pengecualian
Hal pertama yang harus dipahami adalah bahwa Anda harus dapat mengevaluasi jaminan pengecualian yang ditawarkan oleh semua fungsi Anda:
- tidak ada : Kode Anda tidak boleh menawarkan itu. Kode ini akan membocorkan segalanya, dan memecah pada pengecualian pertama yang dilemparkan.
- dasar : Ini adalah jaminan yang paling tidak Anda harus tawarkan, yaitu, jika ada pengecualian, tidak ada sumber daya yang bocor, dan semua objek masih utuh
- kuat : Pemrosesan akan berhasil, atau melempar pengecualian, tetapi jika melempar, maka data akan berada dalam kondisi yang sama seolah-olah pemrosesan tidak dimulai sama sekali (ini memberikan kekuatan transaksional ke C ++)
- nothrow / nofail : Pemrosesan akan berhasil.
Contoh kode
Kode berikut ini sepertinya benar C ++, tetapi sebenarnya, menawarkan jaminan "tidak ada", dan dengan demikian, itu tidak benar:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
X * x = new X() ; // 2. basic : can throw with new and X constructor
t.list.push_back(x) ; // 3. strong : can throw
x->doSomethingThatCanThrow() ; // 4. basic : can throw
}
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:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
std::auto_ptr<X> x(new X()) ; // 2. basic : can throw with new and X constructor
X * px = x.get() ; // 2'. nothrow/nofail
t.list.push_back(px) ; // 3. strong : can throw
x.release() ; // 3'. nothrow/nofail
px->doSomethingThatCanThrow() ; // 4. basic : can throw
}
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:
void doSomething(T & t)
{
// we create "x"
std::auto_ptr<X> x(new X()) ; // 1. basic : can throw with new and X constructor
X * px = x.get() ; // 2. nothrow/nofail
px->doSomethingThatCanThrow() ; // 3. basic : can throw
// we copy the original container to avoid changing it
T t2(t) ; // 4. strong : can throw with T copy-constructor
// we put "x" in the copied container
t2.list.push_back(px) ; // 5. strong : can throw
x.release() ; // 6. nothrow/nofail
if(std::numeric_limits<int>::max() > t2.integer) // 7. nothrow/nofail
t2.integer += 1 ; // 7'. nothrow/nofail
// we swap both containers
t.swap(t2) ; // 8. nothrow/nofail
}
Kami memesan ulang operasi, pertama-tama membuat dan mengatur X
ke nilai yang tepat. Jika operasi apa pun gagal, maka t
tidak dimodifikasi, jadi, operasi 1 hingga 3 dapat dianggap "kuat": Jika sesuatu melempar, t
tidak dimodifikasi, dan X
tidak akan bocor karena dimiliki oleh smart pointer.
Kemudian, kami membuat copy t2
dari t
, 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
dan t2
. Operasi swap harus nothrow dalam C ++, jadi mari kita berharap swap yang Anda tulis T
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, maka t
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":
- [me] Swap tidak akan pernah gagal (bahkan tidak menulis swap melempar)
- [Nobar] Ini adalah rekomendasi yang bagus untuk
swap()
fungsi yang ditulis khusus . Namun perlu dicatat bahwa hal itu std::swap()
dapat gagal berdasarkan operasi yang digunakan secara internal
default 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 untuk vector
,, deque
dan list
tidak akan melempar, sedangkan itu bisa karena map
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
t.integer += 1;
tanpa jaminan bahwa luapan tidak akan terjadi TIDAK terkecuali aman, dan pada kenyataannya mungkin secara teknis memanggil UB! (Signed overflow adalah UB: C ++ 11 5/4 "Jika selama evaluasi ekspresi, hasilnya tidak didefinisikan secara matematis atau tidak dalam kisaran nilai yang dapat diwakili untuk jenisnya, perilaku tidak terdefinisi.") Perhatikan bahwa unsigned integer tidak meluap, tetapi melakukan komputasinya dalam modul kelas ekivalen 2 ^ # bit.
Dionadar mengacu pada baris berikut, yang memang memiliki perilaku tidak terdefinisi.
t.integer += 1 ; // 1. nothrow/nofail
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.