Sebagian besar orang mengatakan tidak pernah membuang pengecualian dari destruktor - melakukan hal itu menghasilkan perilaku yang tidak terdefinisi. Stroustrup menyatakan bahwa "penghancur vektor secara eksplisit memanggil penghancur untuk setiap elemen. Ini menyiratkan bahwa jika penghancur elemen melempar, penghancuran vektor gagal ... Benar-benar tidak ada cara yang baik untuk melindungi terhadap pengecualian yang dilemparkan dari destruktor, sehingga perpustakaan tidak membuat jaminan jika elemen destruktor melempar "(dari Lampiran E3.2) .
Artikel ini tampaknya mengatakan sebaliknya - bahwa melempar destruktor kurang lebih baik.
Jadi pertanyaan saya adalah ini - jika melempar dari destruktor menghasilkan perilaku yang tidak terdefinisi, bagaimana Anda menangani kesalahan yang terjadi selama destruktor?
Jika kesalahan terjadi selama operasi pembersihan, apakah Anda mengabaikannya? Jika itu adalah kesalahan yang berpotensi ditangani tumpukan tetapi tidak tepat di destructor, tidak masuk akal untuk membuang pengecualian dari destructor?
Jelas kesalahan semacam ini jarang terjadi, tetapi mungkin terjadi.
sumber
xyz()
dan jaga destruktor bersih dari logika non-RAII.commit()
metode dipanggil.Jawaban:
Membuang pengecualian dari destruktor berbahaya.
Jika pengecualian lain sudah menyebar aplikasi akan berakhir.
Ini pada dasarnya bermuara pada:
Apa pun yang berbahaya (yaitu yang dapat menimbulkan pengecualian) harus dilakukan melalui metode publik (tidak harus secara langsung). Pengguna kelas Anda kemudian dapat berpotensi menangani situasi ini dengan menggunakan metode publik dan menangkap setiap pengecualian potensial.
Destructor kemudian akan menghabisi objek dengan memanggil metode-metode ini (jika pengguna tidak melakukannya secara eksplisit), tetapi setiap pengecualian melempar ditangkap dan dijatuhkan (setelah mencoba memperbaiki masalah).
Jadi sebenarnya Anda meneruskan tanggung jawab kepada pengguna. Jika pengguna berada dalam posisi untuk memperbaiki pengecualian, mereka akan secara manual memanggil fungsi yang sesuai dan memproses kesalahan. Jika pengguna objek tidak khawatir (karena objek akan dihancurkan) maka destruktor dibiarkan mengurus bisnis.
Sebuah contoh:
std :: fstream
Metode close () berpotensi melempar pengecualian. Destuktor memanggil close () jika file telah dibuka tetapi memastikan bahwa pengecualian tidak menyebar keluar dari destruktor.
Jadi, jika pengguna objek file ingin melakukan penanganan khusus untuk masalah yang terkait dengan penutupan file, mereka akan memanggil secara manual () dan menangani segala pengecualian. Jika di sisi lain mereka tidak peduli maka destructor akan dibiarkan menangani situasi.
Scott Myers memiliki artikel bagus tentang subjek dalam bukunya "Effective C ++"
Edit:
Rupanya juga di "Lebih Efektif C ++"
Butir 11: Mencegah pengecualian dari meninggalkan destruktor
sumber
Melempar keluar dari destruktor dapat mengakibatkan kerusakan, karena destruktor ini mungkin disebut sebagai bagian dari "Stack unwinding". Stack unwinding adalah prosedur yang terjadi ketika pengecualian dilemparkan. Dalam prosedur ini, semua objek yang didorong ke dalam tumpukan sejak "coba" dan sampai pengecualian dilemparkan, akan dihentikan -> destruktor mereka akan dipanggil. Dan selama prosedur ini, lemparan pengecualian lain tidak diperbolehkan, karena tidak mungkin untuk menangani dua pengecualian sekaligus, sehingga, ini akan memicu panggilan untuk membatalkan (), program akan macet dan kontrol akan kembali ke OS.
sumber
throw
tetapi belum menemukancatch
blok untuk itu) dalam hal inistd::terminate
(bukanabort
) dipanggil alih-alih memunculkan eksepsi (baru) (atau melanjutkan stack unwinding).Kami harus membedakan di sini daripada secara membabi buta mengikuti saran umum untuk kasus tertentu .
Perhatikan bahwa berikut ini mengabaikan masalah objek kontainer dan apa yang harus dilakukan dalam menghadapi beberapa d'tor objek di dalam wadah. (Dan itu dapat diabaikan sebagian, karena beberapa benda tidak cocok untuk dimasukkan ke dalam wadah.)
Seluruh masalah menjadi lebih mudah untuk dipikirkan ketika kita membagi kelas menjadi dua jenis. Dtor kelas dapat memiliki dua tanggung jawab yang berbeda:
Jika kita melihat pertanyaan dengan cara ini, maka saya pikir dapat dikatakan bahwa semantik (R) tidak boleh menyebabkan pengecualian dari dtor karena ada a) tidak ada yang dapat kita lakukan dan b) banyak operasi sumber daya gratis tidak bahkan menyediakan pengecekan kesalahan, mis .
void
free(void* p);
Objek dengan semantik (C), seperti objek file yang perlu berhasil mem-flush data atau koneksi database ("scope guarded") yang melakukan commit dalam dtor adalah jenis yang berbeda: Kita dapat melakukan sesuatu tentang kesalahan (di tingkat aplikasi) dan kita benar-benar tidak boleh melanjutkan seolah-olah tidak ada yang terjadi.
Jika kita mengikuti rute RAII dan mengizinkan objek yang memiliki (C) semantik di d'tor mereka, saya pikir kita juga harus mengizinkan untuk kasus aneh di mana d'tor tersebut dapat melempar. Oleh karena itu, Anda tidak boleh meletakkan benda-benda seperti itu ke dalam wadah dan itu juga berarti bahwa program masih bisa
terminate()
jika sebuah commit-dtor melempar ketika pengecualian lain sedang aktif.Berkenaan dengan penanganan kesalahan (semantik Komit / Kembalikan) dan pengecualian, ada pembicaraan yang baik oleh salah satu Andrei Alexandrescu : Penanganan Kesalahan dalam C ++ / Declarative Control Flow (diadakan di NDC 2014 )
Dalam perinciannya, ia menjelaskan bagaimana perpustakaan Folly mengimplementasikan sebuah
UncaughtExceptionCounter
untukScopeGuard
alat mereka .(Saya harus mencatat bahwa orang lain juga punya ide serupa.)
Sementara pembicaraan tidak fokus pada melempar dari d'tor, itu menunjukkan alat yang dapat digunakan hari ini untuk menghilangkan masalah dengan kapan harus melempar dari d'tor.
Di
masa depan,mungkinada fitur std untuk ini,lihat N3614 ,dan diskusi tentang itu .Pembaruan '17: Fitur C ++ 17 std untuk ini sudah
std::uncaught_exceptions
benar. Saya akan segera mengutip artikel cppref:sumber
finally
.finally
adalah seorang dtor. Itu selalu disebut, tidak peduli apa. Untuk perkiraan sintaksis akhirnya, lihat berbagai implementasi scope_guard. Saat ini, dengan mesin yang ada (bahkan dalam standar, apakah itu C ++ 14?) Untuk mendeteksi apakah Dtor diperbolehkan untuk melempar, bahkan dapat dibuat benar-benar aman.finally
secara inheren alat untuk (C). Jika Anda tidak melihat alasannya: pertimbangkan mengapa melegalkan satu sama lain difinally
blok adalah hal yang sah , dan mengapa hal yang sama tidak berlaku untuk destruktor. (Dalam beberapa hal, ini adalah data vs kontrol . Destructor adalah untuk melepaskan data,finally
untuk melepaskan kontrol. Mereka berbeda; sangat disayangkan bahwa C ++ mengikat keduanya.)Pertanyaan sebenarnya untuk bertanya pada diri sendiri tentang melempar dari destructor adalah "Apa yang bisa dilakukan si penelepon dengan ini?" Apakah sebenarnya ada sesuatu yang berguna yang dapat Anda lakukan dengan pengecualian, yang akan mengimbangi bahaya yang diciptakan dengan melemparkan dari destruktor?
Jika saya menghancurkan
Foo
objek, danFoo
destruktor mengeluarkan pengecualian, apa yang bisa saya lakukan dengan itu? Saya bisa mencatatnya, atau saya bisa mengabaikannya. Itu saja. Saya tidak dapat "memperbaiki" itu, karenaFoo
objeknya sudah hilang. Kasus terbaik, saya mencatat pengecualian dan melanjutkan seolah-olah tidak ada yang terjadi (atau menghentikan program). Apakah itu benar-benar layak berpotensi menyebabkan perilaku tidak terdefinisi dengan melemparkan dari destruktor?sumber
std::ofstream
Destructor memerah dan kemudian menutup file. Disk penuh kesalahan dapat terjadi saat memerah, yang Anda benar-benar dapat melakukan sesuatu yang berguna dengan: menunjukkan kepada pengguna dialog kesalahan yang mengatakan bahwa disk keluar dari ruang kosong.Berbahaya, tetapi juga tidak masuk akal dari sudut pandang keterbacaan / pemahaman kode.
Yang harus Anda tanyakan adalah dalam situasi ini
Apa yang harus menangkap pengecualian? Haruskah penelepon foo? Atau haruskah foo menanganinya? Mengapa penelepon foo peduli dengan beberapa objek internal ke foo? Mungkin ada cara bahasa mendefinisikan ini masuk akal, tetapi akan menjadi tidak dapat dibaca dan sulit dimengerti.
Lebih penting lagi, kemana memori untuk Objek pergi? Kemana memori yang dimiliki benda itu pergi? Apakah masih dialokasikan (seolah-olah karena destruktor gagal)? Pertimbangkan juga objek berada di ruang stack , jadi itu jelas hilang terlepas
Kemudian pertimbangkan hal ini
Ketika penghapusan obj3 gagal, bagaimana cara saya benar-benar menghapus dengan cara yang dijamin tidak gagal? Ingatanku sial!
Sekarang perhatikan dalam potongan kode pertama Object hilang secara otomatis karena pada stack sementara Object3 ada di heap. Karena penunjuk ke Object3 hilang, Anda menjadi semacam SOL. Anda memiliki kebocoran memori.
Sekarang salah satu cara aman untuk melakukan sesuatu adalah sebagai berikut
Lihat juga FAQ ini
sumber
int foo()
, Anda dapat menggunakan fungsi-coba-blok untuk membungkus seluruh fungsi foo dalam blok coba-tangkap, termasuk menangkap destruktor, jika Anda ingin melakukannya. Masih bukan pendekatan yang disukai, tetapi itu adalah sesuatu.Dari konsep ISO untuk C ++ (ISO / IEC JTC 1 / SC 22 N 4411)
Jadi destruktor umumnya harus menangkap pengecualian dan tidak membiarkan mereka menyebar keluar dari destruktor.
sumber
Destruktor Anda mungkin mengeksekusi di dalam rantai destruktor lain. Melempar pengecualian yang tidak ditangkap oleh pemanggil langsung Anda dapat meninggalkan beberapa objek dalam keadaan tidak konsisten, sehingga menyebabkan lebih banyak masalah kemudian mengabaikan kesalahan dalam operasi pembersihan.
sumber
Saya berada dalam kelompok yang menganggap bahwa pola "scoped guard" yang dilemparkan ke destruktor berguna dalam banyak situasi - khususnya untuk pengujian unit. Namun, perlu diketahui bahwa dalam C ++ 11, melempar destruktor menghasilkan panggilan ke
std::terminate
karena destruktor secara implisit dijelaskan dengannoexcept
.Andrzej Krzemieński memiliki posting bagus tentang topik destruktor yang melempar:
Dia menunjukkan bahwa C ++ 11 memiliki mekanisme untuk menimpa default
noexcept
untuk destruktor:Akhirnya, jika Anda memutuskan untuk melempar destruktor, Anda harus selalu sadar akan risiko pengecualian ganda (melempar saat tumpukan dilepaskan karena pengecualian). Ini akan menyebabkan panggilan ke
std::terminate
dan jarang yang Anda inginkan. Untuk menghindari perilaku ini, Anda cukup memeriksa apakah sudah ada pengecualian sebelum melempar yang baru menggunakanstd::uncaught_exception()
.sumber
Semua orang telah menjelaskan mengapa melempar destruktor itu mengerikan ... apa yang bisa Anda lakukan? Jika Anda melakukan operasi yang mungkin gagal, buat metode publik terpisah yang melakukan pembersihan dan dapat membuang pengecualian sewenang-wenang. Dalam kebanyakan kasus, pengguna akan mengabaikannya. Jika pengguna ingin memantau keberhasilan / kegagalan pembersihan, mereka cukup memanggil rutin pembersihan eksplisit.
Sebagai contoh:
sumber
Sebagai tambahan untuk jawaban utama, yang baik, komprehensif dan akurat, saya ingin berkomentar tentang artikel yang Anda referensi - yang mengatakan "melempar pengecualian pada destruktor tidak terlalu buruk".
Artikel mengambil garis "apa saja alternatif untuk melempar pengecualian", dan daftar beberapa masalah dengan masing-masing alternatif. Setelah melakukan itu, disimpulkan bahwa karena kita tidak dapat menemukan alternatif yang bebas masalah, kita harus terus melemparkan pengecualian.
Masalahnya adalah bahwa tidak ada masalah yang ada dalam daftar alternatif yang seburuk perilaku pengecualian, yang, mari kita ingat, adalah "perilaku program Anda yang tidak terdefinisi". Beberapa keberatan penulis termasuk "estetis jelek" dan "mendorong gaya buruk". Sekarang mana yang lebih Anda sukai? Sebuah program dengan gaya yang buruk, atau yang menunjukkan perilaku tidak terdefinisi?
sumber
A: Ada beberapa opsi:
Biarkan pengecualian mengalir keluar dari destruktor Anda, terlepas dari apa yang terjadi di tempat lain. Dan dengan demikian waspada (atau bahkan takut) bahwa std :: terminate dapat mengikuti.
Jangan biarkan pengecualian mengalir keluar dari destruktor Anda. Mungkin menulis ke log, beberapa teks merah besar yang buruk jika Anda bisa.
fave saya : Jika
std::uncaught_exception
kembali salah, biarkan Anda pengecualian keluar. Jika hasilnya benar, maka kembali ke pendekatan logging.Tapi apakah bagus untuk melempar d'tor?
Saya setuju dengan sebagian besar di atas bahwa melempar sebaiknya dihindari di destructor, di mana bisa. Tetapi kadang-kadang Anda lebih baik menerimanya bisa terjadi, dan menanganinya dengan baik. Saya akan memilih 3 di atas.
Ada beberapa kasus aneh di mana itu sebenarnya ide bagus untuk dilemparkan dari destructor. Seperti kode kesalahan "harus dicentang". Ini adalah tipe nilai yang dikembalikan dari suatu fungsi. Jika penelepon membaca / memeriksa kode kesalahan yang terkandung, nilai yang dikembalikan merusak diam-diam. Tetapi , jika kode kesalahan kembali belum dibaca pada saat nilai kembali keluar dari ruang lingkup, itu akan membuang beberapa pengecualian, dari destruktornya .
sumber
Saat ini saya mengikuti kebijakan (yang begitu banyak dikatakan) bahwa kelas tidak boleh secara aktif membuang pengecualian dari destruktor mereka tetapi sebaliknya harus menyediakan metode "tutup" publik untuk melakukan operasi yang bisa gagal ...
... tapi saya percaya destruktor untuk kelas tipe kontainer, seperti vektor, tidak boleh menutupi pengecualian yang dilemparkan dari kelas yang dikandungnya. Dalam hal ini, saya benar-benar menggunakan metode "bebas / tutup" yang menyebut dirinya secara rekursif. Ya, kataku secara rekursif. Ada metode untuk kegilaan ini. Perambatan pengecualian bergantung pada keberadaan tumpukan: Jika satu pengecualian terjadi, maka kedua destruktor yang tersisa masih akan berjalan dan pengecualian yang tertunda akan diperbanyak setelah pengembalian rutin, yang bagus. Jika beberapa pengecualian terjadi, maka (tergantung pada kompiler) apakah pengecualian pertama akan menyebar atau program akan berakhir, yang tidak apa-apa. Jika begitu banyak pengecualian terjadi bahwa rekursi meluap tumpukan maka ada sesuatu yang sangat salah, dan seseorang akan mengetahuinya, yang juga oke. Sendiri,
Intinya adalah bahwa wadah tetap netral, dan terserah kelas yang ada untuk memutuskan apakah mereka berperilaku atau berperilaku tidak pantas sehubungan dengan melemparkan pengecualian dari destruktor mereka.
sumber
Tidak seperti konstruktor, di mana melempar pengecualian dapat menjadi cara yang berguna untuk menunjukkan bahwa penciptaan objek berhasil, pengecualian tidak boleh dilemparkan ke destruktor.
Masalahnya terjadi ketika pengecualian dilemparkan dari destruktor selama proses tumpukan unwinding. Jika itu terjadi, kompiler diletakkan dalam situasi di mana ia tidak tahu apakah akan melanjutkan proses pelepasan tumpukan atau menangani pengecualian baru. Hasil akhirnya adalah bahwa program Anda akan segera dihentikan.
Akibatnya, tindakan terbaik adalah tidak menggunakan pengecualian dalam penghancur sama sekali. Tulis pesan ke file log sebagai gantinya.
sumber
Martin Ba (di atas) berada di jalur yang benar - Anda adalah arsitek yang berbeda untuk logika RELEASE dan COMMIT.
Untuk rilis:
Anda harus makan kesalahan. Anda membebaskan memori, menutup koneksi, dll. Tidak ada orang lain dalam sistem yang harus MELIHAT hal-hal itu lagi, dan Anda mengembalikan sumber daya ke OS. Jika sepertinya Anda memerlukan penanganan kesalahan nyata di sini, kemungkinan itu merupakan konsekuensi dari kesalahan desain pada model objek Anda.
Untuk Komit:
Di sinilah Anda menginginkan jenis objek pembungkus RAII yang sama seperti yang disediakan oleh std :: lock_guard untuk mutex. Dengan itu, Anda tidak memasukkan logika komit ke dalam Dtor AT ALL. Anda memiliki API khusus untuk itu, lalu membungkus objek yang akan RAII komit di dalam MEREKA dan menangani kesalahan di sana. Ingat, Anda bisa MENCAPAI pengecualian di destruktor; itu mengeluarkan mereka yang mematikan. Ini juga memungkinkan Anda menerapkan kebijakan dan penanganan kesalahan yang berbeda hanya dengan membangun pembungkus yang berbeda (mis. Std :: unique_lock vs. std :: lock_guard), dan memastikan Anda tidak akan lupa untuk memanggil logika komit - yang merupakan satu-satunya jalan setengah jalan pembenaran yang layak untuk menempatkannya di dtor di tempat 1.
sumber
Masalah utamanya adalah ini: Anda tidak dapat gagal untuk gagal . Apa artinya gagal gagal? Jika melakukan transaksi ke database gagal, dan gagal gagal (gagal mengembalikan), apa yang terjadi dengan integritas data kami?
Karena destruktor dipanggil untuk jalur normal dan luar biasa (gagal), mereka sendiri tidak dapat gagal atau kita "gagal gagal".
Ini adalah masalah yang sulit secara konseptual tetapi seringkali solusinya adalah dengan hanya menemukan cara untuk memastikan bahwa kegagalan tidak dapat gagal. Misalnya, database mungkin menulis perubahan sebelum melakukan ke struktur atau file data eksternal. Jika transaksi gagal, maka struktur file / data dapat dibuang. Yang harus dipastikan adalah melakukan perubahan dari struktur eksternal / mengajukan transaksi atom yang tidak dapat gagal.
Solusi yang paling tepat bagi saya adalah menulis logika non-pembersihan Anda sedemikian rupa sehingga logika pembersihan tidak dapat gagal. Misalnya, jika Anda tergoda untuk membuat struktur data baru untuk membersihkan struktur data yang ada, maka mungkin Anda mungkin ingin membuat struktur tambahan di muka sehingga kami tidak lagi harus membuatnya di dalam destruktor.
Memang ini jauh lebih mudah diucapkan daripada dilakukan, diakui, tapi itu satu-satunya cara yang benar-benar tepat untuk melakukannya. Kadang-kadang saya pikir harus ada kemampuan untuk menulis logika destruktor terpisah untuk jalur eksekusi normal jauh dari yang luar biasa, karena kadang-kadang destruktor merasa sedikit seperti mereka memiliki dua kali lipat tanggung jawab dengan mencoba menangani keduanya (contohnya adalah penjaga ruang lingkup yang membutuhkan pemecatan eksplisit. ; mereka tidak akan memerlukan ini jika mereka bisa membedakan jalur kehancuran yang luar biasa dari yang tidak luar biasa).
Masalah utamanya adalah kita tidak bisa gagal untuk gagal, dan ini adalah masalah desain konseptual yang sulit untuk dipecahkan dengan sempurna dalam semua kasus. Itu menjadi lebih mudah jika Anda tidak terlalu terbungkus dalam struktur kontrol yang kompleks dengan banyak objek kecil berinteraksi satu sama lain, dan sebaliknya memodelkan desain Anda dengan cara yang sedikit lebih massal (contoh: sistem partikel dengan destruktor untuk menghancurkan seluruh partikel sistem, bukan destruktor non-sepele terpisah per partikel). Ketika Anda memodelkan desain Anda pada tingkat yang lebih kasar ini, Anda memiliki lebih sedikit destruktor non-sepele untuk ditangani, dan juga dapat sering membeli apa pun yang diperlukan memori / pemrosesan untuk memastikan bahwa destruktor Anda tidak dapat gagal.
Dan itu salah satu solusi termudah secara alami adalah dengan menggunakan destruktor lebih jarang. Dalam contoh partikel di atas, mungkin saat menghancurkan / mengeluarkan partikel, beberapa hal harus dilakukan yang bisa gagal karena alasan apa pun. Dalam hal itu, alih-alih menggunakan logika seperti itu melalui dtor partikel yang dapat dieksekusi di jalur yang luar biasa, Anda bisa melakukan semuanya dengan sistem partikel ketika menghilangkan partikel. Menghapus partikel mungkin selalu dilakukan selama jalur yang tidak biasa. Jika sistem dihancurkan, mungkin ia bisa membersihkan semua partikel dan tidak repot-repot dengan logika penghilangan partikel individu yang bisa gagal, sementara logika yang bisa gagal hanya dieksekusi selama eksekusi normal sistem partikel ketika mengeluarkan satu atau lebih partikel.
Sering ada solusi seperti itu yang muncul jika Anda menghindari berurusan dengan banyak objek kecil dengan destruktor non-sepele. Di mana Anda bisa terjerat dalam kekacauan di mana tampaknya hampir mustahil untuk menjadi pengecualian-keselamatan adalah ketika Anda terlibat dalam banyak objek kecil yang semuanya memiliki dtor non-sepele.
Ini akan banyak membantu jika nothrow / noexcept benar-benar diterjemahkan ke dalam kesalahan kompiler jika ada sesuatu yang menspesifikasikannya (termasuk fungsi virtual yang harus mewarisi spesifikasi noexcept dari kelas dasarnya) berusaha untuk memohon apa pun yang bisa melempar. Dengan cara ini kita akan dapat menangkap semua hal ini pada waktu kompilasi jika kita benar-benar menulis destruktor yang secara tidak sengaja dapat melempar.
sumber
Atur acara alarm. Biasanya peristiwa alarm adalah bentuk pemberitahuan kegagalan yang lebih baik saat membersihkan objek
sumber