membuang pengecualian dari destruktor

257

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.

Greg Rogers
sumber
36
"Dua pengecualian sekaligus" adalah jawaban yang pasti tetapi itu bukan alasan NYATA. Alasan sebenarnya adalah bahwa pengecualian harus dilemparkan jika dan hanya jika postconditions suatu fungsi tidak dapat dipenuhi. Postcondition dari destructor adalah bahwa objek tidak ada lagi. Ini tidak mungkin tidak terjadi. Karena itu, operasi akhir kehidupan yang cenderung gagal harus disebut sebagai metode terpisah sebelum objek keluar dari cakupan (fungsi yang masuk akal biasanya hanya memiliki satu jalur keberhasilan)
spraff
29
@spraff: Apakah Anda sadar bahwa apa yang Anda katakan menyiratkan "buang RAII"?
Kos
16
@spraff: harus memanggil "metode terpisah sebelum objek keluar dari ruang lingkup" (seperti yang Anda tulis) sebenarnya membuang RAII! Kode yang menggunakan objek seperti itu harus memastikan bahwa metode seperti itu akan dipanggil sebelum destructor dipanggil .. Akhirnya, ide ini tidak membantu sama sekali.
Frunsi
8
@ Frunsi tidak, karena masalah ini berasal dari kenyataan bahwa destructor mencoba melakukan sesuatu di luar pelepasan sumber daya semata. Sangat menggoda untuk mengatakan "Saya selalu ingin akhirnya melakukan XYZ" dan berpikir ini adalah argumen untuk menempatkan logika seperti itu di destructor. Tidak, jangan malas, tulis xyz()dan jaga destruktor bersih dari logika non-RAII.
spraff
6
@ Frunsi Misalnya, melakukan sesuatu ke file tidak selalu OK untuk dilakukan di destructor dari kelas yang mewakili transaksi. Jika komit gagal, sudah terlambat untuk menanganinya ketika semua kode yang terlibat dalam transaksi telah keluar dari ruang lingkup. Destruktor harus membuang transaksi kecuali suatu commit()metode dipanggil.
Nicholas Wilson

Jawaban:

198

Membuang pengecualian dari destruktor berbahaya.
Jika pengecualian lain sudah menyebar aplikasi akan berakhir.

#include <iostream>

class Bad
{
    public:
        // Added the noexcept(false) so the code keeps its original meaning.
        // Post C++11 destructors are by default `noexcept(true)` and
        // this will (by default) call terminate if an exception is
        // escapes the destructor.
        //
        // But this example is designed to show that terminate is called
        // if two exceptions are propagating at the same time.
        ~Bad() noexcept(false)
        {
            throw 1;
        }
};
class Bad2
{
    public:
        ~Bad2()
        {
            throw 1;
        }
};


int main(int argc, char* argv[])
{
    try
    {
        Bad   bad;
    }
    catch(...)
    {
        std::cout << "Print This\n";
    }

    try
    {
        if (argc > 3)
        {
            Bad   bad; // This destructor will throw an exception that escapes (see above)
            throw 2;   // But having two exceptions propagating at the
                       // same time causes terminate to be called.
        }
        else
        {
            Bad2  bad; // The exception in this destructor will
                       // cause terminate to be called.
        }
    }
    catch(...)
    {
        std::cout << "Never print this\n";
    }

}

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

Martin York
sumber
5
"Kecuali kamu tidak keberatan menghentikan aplikasi, kamu mungkin harus menelan kesalahan." - ini mungkin harus menjadi pengecualian (maafkan permainan kata) daripada aturan - yaitu, gagal cepat.
Erik Forbes
15
Saya tidak setuju. Mengakhiri program menghentikan tumpukan pelepasan. Tidak ada lagi destructor yang akan dipanggil. Sumber daya apa pun yang dibuka akan dibiarkan terbuka. Saya pikir menelan pengecualian akan menjadi pilihan yang lebih disukai.
Martin York
20
OS dapat membersihkan sumber daya yang dimatikan pemiliknya. Memori, FileHandles dll. Bagaimana dengan sumber daya yang kompleks: Koneksi DB. Tautan ke ISS yang Anda buka (apakah secara otomatis akan mengirim koneksi tutup)? Saya yakin NASA ingin Anda menutup koneksi dengan bersih!
Martin York
7
Jika suatu aplikasi akan "gagal cepat" dengan membatalkan, pertama-tama itu tidak boleh melempar pengecualian. Jika akan gagal dengan memberikan kembali kontrol tumpukan, itu tidak boleh dilakukan dengan cara yang dapat menyebabkan program dibatalkan. Satu atau yang lain, jangan memilih keduanya.
Tom
2
@LokiAstari Protokol transport yang Anda gunakan untuk berkomunikasi dengan pesawat ruang angkasa tidak dapat menangani koneksi yang terputus? Oke ...
doug65536
54

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.

Gal Goldman
sumber
1
bisa tolong jelaskan bagaimana membatalkan () dipanggil dalam situasi di atas. Berarti kontrol pelaksanaannya masih dengan kompiler C ++
Krishna Oza
1
@Krishna_Oza: Cukup sederhana: setiap kali kesalahan dilempar, kode yang menimbulkan kesalahan memeriksa beberapa bit yang menunjukkan bahwa sistem runtime sedang dalam proses stack unwinding (yaitu, menangani beberapa yang lain throwtetapi belum menemukan catchblok untuk itu) dalam hal ini std::terminate(bukan abort) dipanggil alih-alih memunculkan eksepsi (baru) (atau melanjutkan stack unwinding).
Marc van Leeuwen
53

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:

  • (R) rilis semantik (alias membebaskan memori itu)
  • (C) melakukan semantik ( file flush alias ke disk)

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 UncaughtExceptionCounteruntuk ScopeGuardalat 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 , mungkin ada fitur std untuk ini, lihat N3614 , dan diskusi tentang itu .

Pembaruan '17: Fitur C ++ 17 std untuk ini sudah std::uncaught_exceptionsbenar. Saya akan segera mengutip artikel cppref:

Catatan

Contoh di mana int-pengembalian uncaught_exceptionsdigunakan adalah ... ... pertama-tama membuat objek penjaga dan mencatat jumlah pengecualian yang tidak tertangkap dalam konstruktornya. Output dilakukan oleh destruktor objek penjaga kecuali foo () melempar ( dalam hal ini jumlah pengecualian yang tidak tertangkap dalam destruktor lebih besar dari apa yang diamati oleh konstruktor )

Martin Ba
sumber
6
Sangat setuju. Dan menambahkan satu lagi semantik rollback semantik (Ro). Digunakan secara umum dalam pelindung ruang lingkup. Seperti halnya dalam proyek saya di mana saya mendefinisikan makro ON_SCOPE_EXIT. Kasus tentang kembalikan semantik adalah bahwa apa pun yang berarti dapat terjadi di sini. Jadi kita seharusnya tidak mengabaikan kegagalan itu.
Weipeng L
Saya merasa seperti satu-satunya alasan kami melakukan semantik pada destruktor adalah karena C ++ tidak mendukung finally.
user541686
@Mehrdad: 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.
Martin Ba
1
@ MartinBa: Saya pikir Anda melewatkan inti dari komentar saya, yang mengejutkan karena saya setuju dengan pendapat Anda bahwa (R) dan (C) berbeda. Saya mencoba mengatakan bahwa dtor secara inheren adalah alat untuk (R) dan finallysecara inheren alat untuk (C). Jika Anda tidak melihat alasannya: pertimbangkan mengapa melegalkan satu sama lain di finallyblok 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, finallyuntuk melepaskan kontrol. Mereka berbeda; sangat disayangkan bahwa C ++ mengikat keduanya.)
user541686
1
@Mehrdad: Terlalu lama di sini. Jika mau, Anda dapat membangun argumen di sini: programmers.stackexchange.com/questions/304067/… . Terima kasih.
Martin Ba
21

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 Fooobjek, dan Foodestruktor mengeluarkan pengecualian, apa yang bisa saya lakukan dengan itu? Saya bisa mencatatnya, atau saya bisa mengabaikannya. Itu saja. Saya tidak dapat "memperbaiki" itu, karena Fooobjeknya 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?

Taman Derek
sumber
11
Hanya memperhatikan ... membuang dari dtor tidak pernah Perilaku yang Tidak Terdefinisi. Tentu, ini mungkin memanggil terminate (), tetapi itu adalah perilaku yang ditentukan dengan sangat baik.
Martin Ba
4
std::ofstreamDestructor 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.
Andy
13

Berbahaya, tetapi juga tidak masuk akal dari sudut pandang keterbacaan / pemahaman kode.

Yang harus Anda tanyakan adalah dalam situasi ini

int foo()
{
   Object o;
   // As foo exits, o's destructor is called
}

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

class Object
{ 
   Object2 obj2;
   Object3* obj3;
   virtual ~Object()
   {
       // What should happen when this fails? How would I actually destroy this?
       delete obj3;

       // obj 2 fails to destruct when it goes out of scope, now what!?!?
       // should the exception propogate? 
   } 
};

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

class Socket
{
    virtual ~Socket()
    {
      try 
      {
           Close();
      }
      catch (...) 
      {
          // Why did close fail? make sure it *really* does close here
      }
    } 

};

Lihat juga FAQ ini

Doug T.
sumber
Menghidupkan kembali jawaban ini, ulang: contoh pertama, tentang 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.
tyree731
13

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.

3 Proses memanggil destruktor untuk objek otomatis yang dibangun di jalan dari blok percobaan ke ekspresi lemparan disebut "tumpukan unwinding." [Catatan: Jika destruktor yang dipanggil selama stack unwinding keluar dengan pengecualian, std :: terminate disebut (15.5.1). Jadi destruktor umumnya harus menangkap pengecualian dan tidak membiarkan mereka menyebar keluar dari destruktor. - catatan akhir]

lothar
sumber
1
Tidak menjawab pertanyaan - OP sudah mengetahui hal ini.
Arafangion
2
@Arafangion Saya ragu bahwa dia menyadari hal ini (std :: terminate sedang dipanggil) karena jawaban yang diterima membuat poin yang persis sama.
lothar
@Arafangion seperti dalam beberapa jawaban di sini beberapa orang menyebutkan bahwa batalkan () dipanggil; Atau apakah std :: terminate bergantian memanggil fungsi abort ().
Krishna Oza
7

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.

Franci Penov
sumber
7

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::terminatekarena destruktor secara implisit dijelaskan dengan noexcept.

Andrzej Krzemieński memiliki posting bagus tentang topik destruktor yang melempar:

Dia menunjukkan bahwa C ++ 11 memiliki mekanisme untuk menimpa default noexceptuntuk destruktor:

Dalam C ++ 11, sebuah destruktor secara implisit ditentukan sebagai noexcept. Bahkan jika Anda tidak menambahkan spesifikasi dan mendefinisikan destruktor Anda seperti ini:

  class MyType {
        public: ~MyType() { throw Exception(); }            // ...
  };

Kompiler masih akan menambah spesifikasi noexceptuntuk destruktor Anda. Dan ini berarti bahwa saat destruktor Anda melempar pengecualian, std::terminateakan dipanggil, bahkan jika tidak ada situasi pengecualian ganda. Jika Anda benar-benar bertekad untuk membiarkan destruktor Anda melempar, Anda harus menentukan ini secara eksplisit; Anda memiliki tiga opsi:

  • Tentukan destruktor Anda secara eksplisit sebagai noexcept(false),
  • Mewarisi kelas Anda dari yang lain yang sudah menentukan destruktornya sebagai noexcept(false).
  • Masukkan anggota data non-statis di kelas Anda yang sudah menentukan destruktornya sebagai noexcept(false).

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::terminatedan jarang yang Anda inginkan. Untuk menghindari perilaku ini, Anda cukup memeriksa apakah sudah ada pengecualian sebelum melempar yang baru menggunakan std::uncaught_exception().

GaspardP
sumber
6

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:

class TempFile {
public:
    TempFile(); // throws if the file couldn't be created
    ~TempFile() throw(); // does nothing if close() was already called; never throws
    void close(); // throws if the file couldn't be deleted (e.g. file is open by another process)
    // the rest of the class omitted...
};
Tom
sumber
Saya mencari solusi tetapi mereka mencoba menjelaskan apa yang terjadi dan mengapa. Hanya ingin memperjelas apakah fungsi tutup dipanggil di dalam destruktor?
Jason Liu
5

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?

DJClayworth
sumber
1
Bukan perilaku yang tidak terdefinisi, melainkan pemutusan hubungan langsung.
Marc van Leeuwen
Standar mengatakan 'perilaku tidak terdefinisi'. Perilaku itu sering berakhir tetapi tidak selalu.
DJClayworth
Tidak, baca [exception.terminate] di Exception handling-> Fungsi khusus (yaitu 15.5.1 dalam salinan standar saya, tetapi penomorannya mungkin sudah usang).
Marc van Leeuwen
2

T: Jadi pertanyaan saya adalah ini - jika melempar dari destruktor menghasilkan perilaku yang tidak terdefinisi, bagaimana Anda menangani kesalahan yang terjadi selama destruktor?

A: Ada beberapa opsi:

  1. 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.

  2. Jangan biarkan pengecualian mengalir keluar dari destruktor Anda. Mungkin menulis ke log, beberapa teks merah besar yang buruk jika Anda bisa.

  3. fave saya : Jika std::uncaught_exceptionkembali 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 .

MartinP
sumber
4
Fave Anda adalah sesuatu yang saya coba baru-baru ini, dan ternyata Anda tidak boleh melakukannya. gotw.ca/gotw/047.htm
GManNickG
1

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.

Matius
sumber
1

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.

Devesh Agrawal
sumber
1
Menulis pesan ke file log dapat menyebabkan pengecualian.
Konard
1

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.

pengguna3726672
sumber
1

Jadi pertanyaan saya adalah ini - jika melempar dari destruktor menghasilkan perilaku yang tidak terdefinisi, bagaimana Anda menangani kesalahan yang terjadi selama destruktor?

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 pragmatis mungkin hanya memastikan bahwa kemungkinan kegagalan pada kegagalan secara astronomi tidak mungkin, karena membuat hal-hal yang mustahil untuk gagal gagal bisa menjadi hampir mustahil dalam beberapa kasus.

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.

Energi Naga
sumber
1
Kehancuran adalah kegagalan sekarang?
curiousguy
Saya pikir maksudnya destruktor dipanggil selama kegagalan, untuk membersihkan kegagalan itu. Jadi jika destruktor dipanggil selama pengecualian aktif, maka gagal untuk membersihkan dari kegagalan sebelumnya.
user2445507
0

Atur acara alarm. Biasanya peristiwa alarm adalah bentuk pemberitahuan kegagalan yang lebih baik saat membersihkan objek

MRN
sumber