Pengecualian yang menghancurkan

Sekali lagi tentang mengapa membuang pengecualian pada destruktor itu buruk


Banyak pakar C ++ (misalnya, Herb Sutter ) mengajarkan kepada kita bahwa melemparkan pengecualian pada destruktor adalah buruk, karena Anda dapat masuk ke destruktor selama promosi tumpukan dengan pengecualian yang sudah dilemparkan, dan jika pengecualian lain dilemparkan pada titik ini, std akan dipanggil :: terminate () . Standar bahasa C ++ 17 (selanjutnya saya merujuk pada versi draft N4713 yang tersedia bebas) tentang topik ini memberi tahu kita hal berikut:


18.5.1 Fungsi std :: terminate () [exception.terminate]

1 Dalam beberapa situasi, penanganan pengecualian harus ditinggalkan untuk teknik penanganan kesalahan yang kurang halus. [Catatan:

Situasi-situasi ini adalah:

...

(1.4) ketika penghancuran objek selama tumpukan unwinding (18.2) berakhir dengan melemparkan pengecualian, atau

...

- catatan akhir]

Mari kita periksa contoh sederhana:


#include <iostream> class PrintInDestructor { public: ~PrintInDestructor() noexcept { std::cerr << "~PrintInDestructor() invoked\n"; } }; void throw_int_func() { std::cerr << "throw_int_func() invoked\n"; throw 1; } class ThrowInDestructor { public: ~ThrowInDestructor() noexcept(false) { std::cerr << "~ThrowInDestructor() invoked\n"; throw_int_func(); } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowInDestructor bad; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* c) { std::cerr << "Catched const char* exception: " << c << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; } 

Hasil:


 ~ThrowInDestructor() invoked throw_int_func() invoked ~PrintInDestructor() invoked terminate called after throwing an instance of 'int' Aborted 

Perhatikan bahwa destruktor PrintInDestructor masih dipanggil, yaitu setelah melempar pengecualian kedua, promosi tumpukan tidak terganggu. Standar (paragraf yang sama 18.5.1) tentang hal ini mengatakan sebagai berikut:


2 ... Dalam situasi di mana tidak ada pawang yang cocok ditemukan,
itu adalah implementasi yang ditentukan apakah stack dibatalkan atau tidak sebelum std :: terminate () dipanggil. Masuk
situasi di mana pencarian pawang (18.3) bertemu blok terluar dari fungsi dengan a
spesifikasi pengecualian non-melempar (18.4), itu adalah implementasi-ditentukan apakah stack dibatalkan,
membatalkan sebagian, atau tidak membatalkan sama sekali sebelum std :: terminate () disebut ...

Saya menguji contoh ini pada beberapa versi GCC (8.2, 7.3) dan Dentang (6.0, 5.0), di mana-mana promosi stack berlanjut. Jika Anda menemukan kompiler di mana implementasi yang didefinisikan berbeda, silakan menulis tentang itu di komentar.


Juga harus dicatat bahwa std :: terminate () dipanggil ketika stack dibatalkan hanya ketika pengecualian dilempar keluar dari destructor. Jika ada blok coba / tangkap di dalam destruktor yang menangkap pengecualian dan tidak melempar lebih jauh, ini tidak mengganggu promosi tumpukan pengecualian eksternal.


 class ThrowCatchInDestructor { public: ~ThrowCatchInDestructor() noexcept(false) { try { throw_int_func(); } catch (int i) { std::cerr << "Catched int in ~ThrowCatchInDestructor(): " << i << "\n"; } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowCatchInDestructor good; std::cerr << "ThrowCatchInDestructor instance created\n"; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; } 

menampilkan


 ThrowCatchInDestructor instance created throw_int_func() invoked Catched int in ~ThrowCatchInDestructor(): 1 ~PrintInDestructor() invoked Catched const char* exception: BANG! 

Bagaimana cara menghindari situasi yang tidak menyenangkan? Secara teori, semuanya sederhana: jangan pernah melempar pengecualian pada destruktor. Namun, dalam praktiknya tidak begitu sederhana untuk dengan indah dan elegan mewujudkan persyaratan sederhana ini.


Jika Anda tidak bisa, tetapi benar-benar ingin ...


Saya akan segera mencatat bahwa saya tidak berusaha membenarkan melemparkan pengecualian dari destruktor, dan mengikuti Sutter, Meyers, dan guru C ++ lainnya, saya mendorong Anda untuk mencoba tidak pernah melakukan ini (setidaknya dalam kode baru). Namun demikian, dalam praktik nyata, seorang programmer mungkin menemukan kode warisan, yang tidak begitu mudah mengarah pada standar tinggi. Selain itu, teknik yang sering dijelaskan di bawah ini dapat berguna selama proses debugging.

Sebagai contoh, kami sedang mengembangkan perpustakaan dengan kelas wrapper yang merangkum kerja dengan sumber daya tertentu. Sesuai dengan prinsip-prinsip RAII, kami mengambil sumber daya di konstruktor dan harus membebaskannya di destruktor. Tetapi bagaimana jika upaya untuk membebaskan sumber daya gagal? Opsi untuk mengatasi masalah ini:


  • Abaikan kesalahannya. Buruk, karena kami menyembunyikan masalah yang dapat mempengaruhi bagian lain dari sistem.
  • Tulis ke log. Lebih baik daripada hanya mengabaikannya, tapi tetap saja buruk, karena perpustakaan kami tidak tahu apa-apa tentang kebijakan logging yang diadopsi dalam sistem yang menggunakannya. Log standar dapat dialihkan ke / dev / null, akibatnya, sekali lagi, kita tidak akan melihat kesalahan.
  • Ambil pelepasan sumber daya ke fungsi terpisah yang mengembalikan nilai atau melempar pengecualian, dan memaksa pengguna kelas untuk menyebutnya sendiri. Itu buruk, karena pengguna bisa lupa melakukan ini sama sekali, dan kami akan menerima kebocoran sumber daya.
  • Lempar pengecualian. Bagus dalam kasus biasa, seperti pengguna kelas dapat menangkap pengecualian dan mendapatkan informasi tentang kesalahan dengan cara standar. Buruk selama promosi tumpukan, sebagai mengarah ke std :: terminate () .

Bagaimana memahami apakah kita sedang dalam proses mempromosikan tumpukan dengan pengecualian atau tidak? Di C ++, ada fungsi khusus std :: uncaught_exception () untuk ini . Dengan bantuannya, kita dapat dengan aman melempar pengecualian dalam situasi normal, atau melakukan sesuatu yang kurang benar, tetapi tidak mengarah pada melemparkan pengecualian selama promosi tumpukan.


 class ThrowInDestructor { public: ~ThrowInDestructor() noexcept(false) { if (std::uncaught_exception()) { std::cerr << "~ThrowInDestructor() stack unwinding, not throwing\n"; } else { std::cerr << "~ThrowInDestructor() normal case, throwing\n"; throw_int_func(); } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowInDestructor normal; std::cerr << "ThrowInDestructor normal destruction\n"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } try { ThrowInDestructor stack_unwind; std::cerr << "ThrowInDestructor stack unwinding\n"; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; } 

Hasil:


 ThrowInDestructor normal destruction ~ThrowInDestructor() normal case, throwing throw_int_func() invoked ~PrintInDestructor() invoked Catched int exception: 1 ThrowInDestructor stack unwinding ~ThrowInDestructor() stack unwinding, not throwing ~PrintInDestructor() invoked Catched const char* exception: BANG! 

Perhatikan bahwa fungsi std :: uncaught_exception () sudah tidak digunakan lagi sejak C ++ Standard 17, oleh karena itu, untuk mengkompilasi contoh, vorning yang sesuai harus ditekan (lihat repositori dengan contoh-contoh dari artikel ).


Masalah dengan fungsi ini adalah ia memeriksa apakah kita sedang dalam proses memutar stack dengan pengecualian. Tetapi tidak mungkin untuk memahami jika destructor saat ini dipanggil selama proses promosi tumpukan. Akibatnya, jika ada promosi tumpukan, tetapi destruktor dari beberapa objek disebut normal, std :: uncaught_exception () masih akan mengembalikan true .


 class MayThrowInDestructor { public: ~MayThrowInDestructor() noexcept(false) { if (std::uncaught_exception()) { std::cerr << "~MayThrowInDestructor() stack unwinding, not throwing\n"; } else { std::cerr << "~MayThrowInDestructor() normal case, throwing\n"; throw_int_func(); } } }; class ThrowCatchInDestructor { public: ~ThrowCatchInDestructor() noexcept(false) { try { MayThrowInDestructor may_throw; } catch (int i) { std::cerr << "Catched int in ~ThrowCatchInDestructor(): " << i << "\n"; } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowCatchInDestructor stack_unwind; std::cerr << "ThrowInDestructor stack unwinding\n"; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; } 

Hasil:


 ThrowInDestructor stack unwinding ~MayThrowInDestructor() stack unwinding, not throwing ~PrintInDestructor() invoked Catched const char* exception: BANG! 

Dalam Standar C ++ 17 yang baru, fungsi std :: uncaught_exceptions () diperkenalkan untuk menggantikan std :: uncaught_exception () (perhatikan bentuk jamak), yang alih-alih nilai Boolean mengembalikan jumlah pengecualian yang saat ini aktif (di sini adalah justifikasi terperinci).


Ini adalah bagaimana masalah yang dijelaskan di atas diselesaikan dengan std :: uncaught_exceptions () :


 class MayThrowInDestructor { public: MayThrowInDestructor() : exceptions_(std::uncaught_exceptions()) {} ~MayThrowInDestructor() noexcept(false) { if (std::uncaught_exceptions() > exceptions_) { std::cerr << "~MayThrowInDestructor() stack unwinding, not throwing\n"; } else { std::cerr << "~MayThrowInDestructor() normal case, throwing\n"; throw_int_func(); } } private: int exceptions_; }; class ThrowCatchInDestructor { public: ~ThrowCatchInDestructor() noexcept(false) { try { MayThrowInDestructor may_throw; } catch (int i) { std::cerr << "Catched int in ~ThrowCatchInDestructor(): " << i << "\n"; } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowCatchInDestructor stack_unwind; std::cerr << "ThrowInDestructor stack unwinding\n"; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; } 

Hasil:


 ThrowInDestructor stack unwinding ~MayThrowInDestructor() normal case, throwing throw_int_func() invoked Catched int in ~ThrowCatchInDestructor(): 1 ~PrintInDestructor() invoked Catched const char* exception: BANG! 

Ketika saya benar-benar ingin sekali melemparkan beberapa pengecualian sekaligus


std :: uncaught_exceptions () menghindari memanggil std :: terminate () , tetapi tidak membantu menangani beberapa pengecualian dengan benar. Idealnya, saya ingin memiliki mekanisme yang memungkinkan saya untuk menyimpan semua pengecualian yang dilemparkan, dan kemudian memprosesnya di satu tempat.


Saya ingin mengingatkan sekali lagi bahwa mekanisme yang diusulkan oleh saya di bawah ini hanya berfungsi untuk menunjukkan konsep dan tidak direkomendasikan untuk digunakan dalam kode industri nyata.

Inti dari idenya adalah menangkap pengecualian dan menyimpannya dalam wadah, lalu mendapatkan dan memprosesnya satu per satu. Untuk menyimpan objek pengecualian, C ++ memiliki tipe khusus std :: exception_ptr . Tipe struktur dalam Standar tidak diungkapkan, tetapi dikatakan bahwa itu pada dasarnya shared_ptr per objek pengecualian.


Lalu bagaimana memproses pengecualian ini? Ada fungsi std :: rethrow_exception () untuk ini , yang mengambil pointer std :: exception_ptr dan melempar pengecualian yang sesuai. Kita hanya perlu menangkapnya dengan bagian tangkapan yang sesuai dan memprosesnya, setelah itu kita bisa beralih ke objek pengecualian berikutnya.


 using exceptions_queue = std::stack<std::exception_ptr>; // Get exceptions queue for current thread exceptions_queue& get_queue() { thread_local exceptions_queue queue_; return queue_; } // Invoke functor and save exception in queue void safe_invoke(std::function<void()> f) noexcept { try { f(); } catch (...) { get_queue().push(std::current_exception()); } } class ThrowInDestructor { public: ~ThrowInDestructor() noexcept { std::cerr << "~ThrowInDestructor() invoked\n"; safe_invoke([]() { throw_int_func(); }); } private: PrintInDestructor member_; }; int main(int, char**) { safe_invoke([]() { ThrowInDestructor bad; throw "BANG!"; }); auto& q = get_queue(); while (!q.empty()) { try { std::exception_ptr ex = q.top(); q.pop(); if (ex != nullptr) { std::rethrow_exception(ex); } } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } } return 0; } 

Hasil:


 ~ThrowInDestructor() invoked throw_int_func() invoked ~PrintInDestructor() invoked Catched const char* exception: BANG! Catched int exception: 1 

Dalam contoh di atas, tumpukan digunakan untuk menyimpan objek pengecualian, namun penanganan pengecualian akan dilakukan sesuai dengan prinsip FIFO (yaitu, secara logis ini adalah antrian - pengecualian yang dilempar terlebih dahulu akan menjadi yang pertama diproses).


Kesimpulan


Melontarkan pengecualian pada destruktor objek benar-benar ide yang buruk, dan dalam kode baru apa pun saya sangat menyarankan untuk tidak melakukan ini dengan mendeklarasikan destruktor noexcept . Namun, dengan dukungan dan debugging kode lama, mungkin ada kebutuhan untuk menangani pengecualian yang dilemparkan dari destruktor, termasuk selama promosi stack, dan C ++ modern memberi kita mekanisme untuk ini. Saya harap ide-ide yang disajikan dalam artikel ini akan membantu Anda di sepanjang jalan yang sulit ini.


Referensi


Repositori dengan contoh-contoh dari artikel

Source: https://habr.com/ru/post/id433944/


All Articles