Pernyataan FAQ pengecualian isocpp.org menyatakan
Jangan gunakan lemparan untuk menunjukkan kesalahan pengkodean dalam penggunaan suatu fungsi. Gunakan assert atau mekanisme lain untuk mengirim proses ke debugger atau untuk menghentikan proses dan mengumpulkan crash dump untuk pengembang untuk debug.
Di sisi lain pustaka standar mendefinisikan std :: logic_error dan semua turunannya, yang bagi saya sepertinya mereka harus tangani, selain hal-hal lain, kesalahan pemrograman. Apakah meneruskan string kosong ke std :: stof (akan membuang invalid_argument) bukan kesalahan pemrograman? Apakah meneruskan string yang berisi karakter berbeda dari '1' / '0' ke std :: bitset (akan membuang invalid_argument) bukan kesalahan pemrograman? Apakah memanggil std :: bitset :: diset dengan indeks yang tidak valid (akan membuang out_of_range) bukan kesalahan pemrograman? Jika tidak, lalu apa kesalahan pemrograman yang akan diuji? Konstruktor berbasis string std :: bitset hanya ada sejak C ++ 11, jadi seharusnya dirancang dengan penggunaan pengecualian idiomatik. Di sisi lain saya sudah punya orang mengatakan kepada saya logic_error pada dasarnya tidak boleh digunakan sama sekali.
Aturan lain yang sering muncul dengan pengecualian adalah "hanya gunakan pengecualian dalam keadaan luar biasa". Tapi bagaimana fungsi perpustakaan seharusnya tahu keadaan mana yang luar biasa? Untuk beberapa program, tidak dapat membuka file mungkin luar biasa. Bagi yang lain, tidak dapat mengalokasikan memori mungkin tidak luar biasa. Dan ada 100-an kasus di antaranya. Tidak dapat membuat soket? Tidak dapat terhubung, atau menulis data ke soket atau file? Tidak dapat menguraikan input? Mungkin luar biasa, mungkin juga tidak. Fungsi itu sendiri pasti tidak dapat secara umum tahu, ia tidak tahu di mana konteks itu dipanggil.
Jadi, bagaimana saya harus memutuskan apakah saya harus menggunakan pengecualian atau tidak untuk fungsi tertentu? Tampak bagi saya bahwa satu-satunya cara yang benar-benar konsisten adalah menggunakannya untuk setiap dan semua penanganan kesalahan, atau untuk apa-apa. Dan jika saya menggunakan perpustakaan standar, pilihan itu dibuat untuk saya.
sumber
Jawaban:
Pertama, saya merasa berkewajiban untuk menunjukkan bahwa
std::exception
dan anak-anaknya dirancang sejak lama. Ada sejumlah bagian yang mungkin (hampir pasti) akan berbeda jika sedang dirancang hari ini.Jangan salah paham: ada bagian dari desain yang bekerja dengan cukup baik, dan merupakan contoh yang cukup bagus tentang cara mendesain hierarki pengecualian untuk C ++ (mis. Kenyataan bahwa, tidak seperti kebanyakan kelas lain, mereka semua berbagi akar yang sama).
Melihat secara khusus
logic_error
, kami memiliki sedikit teka-teki. Di satu sisi, jika Anda memiliki pilihan yang masuk akal dalam hal ini, saran yang Anda kutip adalah benar: umumnya lebih baik gagal secepat dan sesering mungkin sehingga dapat dibantah dan diperbaiki.Baik atau buruk, sulit untuk mendefinisikan perpustakaan standar di sekitar apa yang seharusnya Anda lakukan secara umum . Jika ia menetapkan ini untuk keluar dari program (misalnya, panggilan
abort()
) ketika diberi input yang salah, itu akan selalu terjadi untuk keadaan itu - dan sebenarnya ada beberapa keadaan di mana ini mungkin bukan hal yang benar untuk dilakukan , setidaknya dalam kode yang digunakan.Itu akan berlaku dalam kode dengan persyaratan real-time (setidaknya lunak), dan penalti minimal untuk output yang salah. Misalnya, pertimbangkan program obrolan. Jika itu decoding beberapa data suara, dan mendapat beberapa input yang salah, kemungkinan pengguna akan jauh lebih bahagia untuk hidup dengan milidetik statis dalam output daripada program yang baru saja dimatikan sepenuhnya. Demikian juga saat melakukan pemutaran video, mungkin lebih dapat diterima untuk hidup dengan menghasilkan nilai yang salah untuk beberapa piksel untuk satu atau dua bingkai daripada membiarkan program keluar karena aliran input rusak.
Adapun apakah akan menggunakan pengecualian untuk melaporkan jenis kesalahan tertentu: Anda benar - operasi yang sama mungkin memenuhi syarat sebagai pengecualian atau tidak, tergantung pada bagaimana itu digunakan.
Di sisi lain, Anda juga salah - menggunakan perpustakaan standar tidak (memaksakan) keputusan itu pada Anda. Dalam hal membuka file, biasanya Anda menggunakan iostream. Iostreams juga bukan desain terbaru dan terhebat, tetapi dalam hal ini mereka melakukan sesuatu dengan benar: mereka membiarkan Anda mengatur mode kesalahan, sehingga Anda dapat mengontrol apakah gagal membuka file dengan hasil pengecualian dilemparkan atau tidak. Jadi, jika Anda memiliki file yang benar-benar diperlukan untuk aplikasi Anda, dan gagal membukanya berarti Anda harus mengambil tindakan perbaikan yang serius, maka Anda dapat meminta pengecualian jika gagal membuka file itu. Untuk sebagian besar file, yang akan Anda coba buka, jika tidak ada atau tidak dapat diakses, mereka hanya akan gagal (ini defaultnya).
Adapun cara Anda memutuskan: Saya tidak berpikir ada jawaban yang mudah. Baik atau buruk, "keadaan luar biasa" tidak selalu mudah untuk diukur. Meskipun ada beberapa kasus yang mudah untuk diputuskan harus luar biasa, ada (dan mungkin selalu akan) kasus-kasus yang terbuka untuk dipertanyakan, atau membutuhkan pengetahuan tentang konteks yang berada di luar domain fungsi yang ada. Untuk kasus-kasus seperti itu, setidaknya mungkin layak untuk mempertimbangkan desain yang kira-kira mirip dengan bagian iostreams ini, di mana pengguna dapat memutuskan apakah kegagalan menghasilkan pengecualian yang dilemparkan atau tidak. Atau, sangat mungkin untuk memiliki dua set fungsi yang terpisah (atau kelas, dll.), Yang salah satunya akan melempar pengecualian untuk menunjukkan kegagalan, yang lainnya menggunakan cara lain. Jika Anda pergi rute itu,
sumber
Anda mungkin tidak mempercayainya, tetapi, beberapa coders C ++ yang berbeda tidak setuju. Itu sebabnya FAQ mengatakan satu hal tetapi perpustakaan standar tidak setuju.
Pendukung FAQ mogok karena itu akan lebih mudah untuk debug. Jika Anda mogok dan mendapatkan dump inti, Anda akan memiliki status aplikasi yang tepat. Jika Anda melempar pengecualian, Anda akan kehilangan banyak kondisi itu.
Pustaka standar mengambil teori bahwa memberi koder kemampuan untuk menangkap dan kemungkinan menangani kesalahan lebih penting daripada debuggabilitas.
Idenya di sini adalah bahwa jika fungsi Anda tidak tahu apakah situasinya luar biasa atau tidak, itu tidak boleh membuang pengecualian. Seharusnya mengembalikan status kesalahan melalui beberapa mekanisme lain. Setelah mencapai titik di program di mana ia tahu negara luar biasa, maka ia harus membuang pengecualian.
Tapi ini punya masalah sendiri. Jika keadaan kesalahan dikembalikan dari suatu fungsi, Anda mungkin tidak ingat untuk memeriksanya dan kesalahan akan berlalu dengan diam-diam. Hal ini menyebabkan beberapa orang meninggalkan pengecualian adalah aturan yang luar biasa demi membuang pengecualian untuk segala bentuk kesalahan.
Secara keseluruhan, kuncinya adalah bahwa orang yang berbeda memiliki ide yang berbeda tentang kapan harus melempar pengecualian. Anda tidak akan menemukan ide kohesif tunggal. Meskipun beberapa orang akan secara dogmatis menyatakan bahwa ini atau itu adalah cara yang tepat untuk menangani pengecualian, tidak ada satu pun teori yang disepakati.
Anda bisa melempar pengecualian:
dan temukan seseorang di internet yang setuju dengan Anda. Anda harus mengadopsi gaya yang sesuai untuk Anda.
sumber
Banyak jawaban baik lainnya telah ditulis, saya hanya ingin menambahkan poin singkat.
Jawaban tradisional, terutama ketika FAQ ISO C ++ ditulis, terutama membandingkan "Pengecualian C ++" vs. "kode pengembalian gaya-C". Opsi ketiga, "kembalikan beberapa jenis nilai komposit, misalnya a
struct
atauunion
, atau saat ini,boost::variant
atau (yang diusulkan)std::expected
, tidak dipertimbangkan.Sebelum ke C ++ 11 opsi "return a composite type" biasanya sangat lemah. Karena, tidak ada semantik yang bergerak sehingga menyalin sesuatu masuk dan keluar dari suatu struktur berpotensi sangat mahal. Sangat penting pada saat itu dalam bahasa untuk menata kode Anda ke RVO untuk mendapatkan kinerja terbaik. Pengecualian seperti cara mudah untuk secara efektif mengembalikan tipe komposit, padahal sebaliknya itu akan sangat sulit.
IMO, setelah C ++ 11, opsi ini "mengembalikan serikat terdiskriminasi", mirip dengan idiom yang
Result<T, E>
digunakan di Rust saat ini, harus lebih disukai dalam kode C ++. Kadang-kadang itu benar-benar gaya yang lebih sederhana dan lebih nyaman untuk menunjukkan kesalahan. Dengan pengecualian, selalu ada kemungkinan ini bahwa fungsi yang tidak melempar sebelum tiba-tiba bisa mulai melempar setelah refactor, dan programmer tidak selalu mendokumentasikan hal-hal seperti itu dengan baik. Ketika kesalahan diindikasikan sebagai bagian dari nilai balik dalam serikat terdiskriminasi, itu sangat mengurangi kemungkinan bahwa programmer hanya akan mengabaikan kode kesalahan, yang merupakan kritik biasa dari penanganan kesalahan gaya-C.Biasanya
Result<T, E>
bekerja seperti boost seperti opsional. Anda dapat menguji, menggunakanoperator bool
, apakah itu nilai atau kesalahan. Dan kemudian gunakan katakanoperator *
untuk mengakses nilai, atau fungsi "get" lainnya. Biasanya akses itu tidak dicentang, untuk kecepatan. Tapi Anda bisa membuatnya sehingga dalam membangun debug, akses menjadi diperiksa dan pernyataan memastikan bahwa sebenarnya ada nilai dan bukan kesalahan. Dengan cara ini siapa pun yang tidak memeriksa kesalahan dengan benar akan mendapatkan penegasan yang keras daripada beberapa masalah yang lebih berbahaya.Keuntungan tambahan adalah bahwa, tidak seperti pengecualian di mana jika tidak tertangkap hanya terbang tumpukan jarak sewenang-wenang, dengan gaya ini, ketika suatu fungsi mulai menandakan kesalahan di mana tidak sebelumnya, Anda tidak dapat mengkompilasi kecuali kode diubah untuk menanganinya. Ini membuat masalah lebih keras - masalah tradisional "pengecualian tanpa tertangkap" menjadi lebih seperti kesalahan waktu kompilasi daripada kesalahan runtime.
Saya sudah menjadi penggemar berat gaya ini. Biasanya, saya saat ini menggunakan ini atau pengecualian. Tapi saya mencoba membatasi pengecualian untuk masalah besar. Untuk sesuatu seperti kesalahan parse, saya mencoba kembali
expected<T>
misalnya. Hal-hal sepertistd::stoi
danboost::lexical_cast
yang melempar pengecualian C ++ dalam hal masalah yang relatif kecil "string tidak dapat dikonversi ke angka" tampaknya sangat tidak enak bagi saya saat ini.sumber
std::expected
masih proposal yang tidak diterima, kan?exception_ptr
, atau apakah Anda hanya ingin menggunakan beberapa jenis struktur atau sesuatu seperti itu.[[nodiscard]] attribute
akan membantu untuk pendekatan penanganan kesalahan ini karena memastikan bahwa Anda tidak mengabaikan hasil kesalahan secara tidak sengaja.except_ptr
) Anda harus melempar pengecualian secara internal. Secara pribadi saya pikir alat seperti itu harus bekerja sepenuhnya independen dari eksekusi. Hanya sebuah komentar.Ini adalah masalah yang sangat subyektif, karena merupakan bagian dari desain. Dan karena desain pada dasarnya seni, saya lebih suka membahas hal-hal ini daripada berdebat (saya tidak mengatakan Anda berdebat).
Bagi saya, kasus luar biasa ada dua macam - kasus yang berhubungan dengan sumber daya dan kasus yang berhubungan dengan operasi kritis. Apa yang dapat dianggap kritis tergantung pada masalah yang dihadapi, dan, dalam banyak kasus, pada sudut pandang programmer.
Kegagalan untuk memperoleh sumber daya adalah kandidat teratas untuk melempar pengecualian. Sumber daya dapat berupa memori, file, koneksi jaringan atau apa pun berdasarkan masalah dan platform Anda. Sekarang, apakah kegagalan untuk merilis sumber daya memerlukan pengecualian? Nah, itu lagi tergantung. Saya belum melakukan apa pun di mana melepaskan memori gagal, jadi saya tidak yakin tentang skenario itu. Namun, menghapus file sebagai bagian dari rilis sumber daya bisa gagal, dan gagal bagi saya, dan kegagalan itu biasanya dikaitkan dengan proses lain setelah membuatnya dibuka di aplikasi multi-proses. Saya kira sumber daya lain bisa gagal selama rilis seperti file, dan biasanya cacat desain yang menyebabkan masalah ini, jadi memperbaikinya akan lebih baik daripada melempar pengecualian.
Kemudian datang memperbarui sumber daya. Poin ini, setidaknya bagi saya, terkait erat dengan aspek operasi kritis aplikasi. Bayangkan sebuah
Employee
kelas dengan fungsiUpdateDetails(std::string&)
yang memodifikasi detail berdasarkan string yang dipisahkan koma. Mirip dengan pelepasan memori yang gagal, saya merasa sulit untuk membayangkan penugasan nilai variabel anggota gagal karena kurangnya pengalaman dalam domain tersebut di mana ini mungkin terjadi. Namun, fungsi sepertiUpdateDetailsAndUpdateFile(std::string&)
yang sesuai dengan namanya diharapkan gagal. Inilah yang saya sebut operasi kritis.Sekarang, Anda harus melihat apakah yang disebut operasi kritis itu menjamin pengecualian untuk dilempar. Maksud saya, apakah memperbarui file terjadi pada akhirnya, seperti pada destruktor, atau apakah itu hanya panggilan paranoid yang dibuat setelah setiap pembaruan? Apakah ada mekanisme mundur yang menulis objek tidak tertulis secara teratur? Apa yang saya katakan adalah, Anda harus mengevaluasi kekritisan operasi.
Jelas, ada banyak operasi kritis yang tidak terkait dengan sumber daya. Jika
UpdateDetails()
diberi data yang salah, itu tidak akan memperbarui detail dan kegagalan harus diketahui, jadi Anda akan melemparkan pengecualian di sini. Tapi bayangkan fungsi sepertiGiveRaise()
. Sekarang, jika karyawan tersebut beruntung memiliki bos berambut runcing dan tidak akan mendapatkan kenaikan gaji (dalam hal pemrograman, nilai beberapa variabel menghentikan ini terjadi), fungsi dasarnya gagal. Apakah Anda akan melemparkan pengecualian di sini? Apa yang saya katakan adalah, Anda harus mengevaluasi perlunya pengecualian.Bagi saya, konsistensi dalam hal pendekatan desain saya daripada kegunaan kelas saya. Maksud saya adalah, saya tidak berpikir dalam hal 'semua fungsi Get harus melakukan ini dan semua fungsi Pembaruan harus untuk ini' tetapi melihat apakah fungsi tertentu menarik ide tertentu dalam pendekatan saya. Pada permukaannya, kelas-kelas bisa terlihat agak 'serampangan' tetapi setiap kali pengguna (kebanyakan kolega dari tim lain) berteriak-teriak atau bertanya tentang hal itu, saya akan menjelaskan dan mereka tampak puas.
Saya melihat banyak orang yang pada dasarnya mengganti nilai kembali dengan pengecualian karena mereka menggunakan C ++ dan bukan C, dan itu memberikan 'pemisahan yang bagus dari penanganan kesalahan' dll. Dan mendesak saya untuk berhenti 'mencampur' bahasa dll. Saya biasanya menjauh dari orang-orang seperti itu.
sumber
Pertama, seperti orang lain telah menyatakan, hal-hal yang tidak yang jelas dipotong di C ++, IMHO terutama karena persyaratan dan pembatasan yang agak lebih bervariasi dalam C ++ dari bahasa lain, esp. C # dan Java, yang memiliki masalah pengecualian "mirip".
Saya akan mengungkap contoh std :: stof:
Dasar kontrak , seperti yang saya lihat, dari fungsi ini adalah bahwa ia mencoba untuk mengkonversi argumen itu untuk pelampung, dan setiap kegagalan untuk melakukannya dilaporkan oleh pengecualian. Kedua pengecualian yang mungkin berasal dari
logic_error
tetapi tidak dalam arti kesalahan programer tetapi dalam arti "input tidak dapat, pernah dikonversi menjadi float".Di sini, dapat dikatakan bahwa a
logic_error
digunakan untuk menunjukkan bahwa, mengingat bahwa (runtime) input, selalu merupakan kesalahan untuk mencoba mengubahnya - tetapi tugas fungsi untuk menentukan itu dan memberi tahu Anda (melalui pengecualian).Catatan: Dalam pandangan itu, a
runtime_error
dapat dilihat sebagai sesuatu yang, diberikan input yang sama ke suatu fungsi, secara teoritis dapat berhasil untuk berbagai proses. (mis. operasi file, akses DB, dll.)Catatan Sisi Lebih Lanjut: Pustaka regex C ++ memilih untuk menurunkan kesalahan itu dari
runtime_error
meskipun ada kasus di mana ia dapat diklasifikasikan sama seperti di sini (pola regex tidak valid).Ini hanya menunjukkan, IMHO, bahwa pengelompokan ke dalam
logic_
atauruntime_
kesalahan cukup kabur di C ++ dan tidak terlalu banyak membantu dalam kasus umum (*) - jika Anda perlu menangani kesalahan tertentu, Anda mungkin perlu menangkap lebih rendah dari keduanya.(*): Itu bukan untuk mengatakan bahwa sepotong kode tidak boleh konsisten, tetapi apakah Anda melempar
runtime_
ataulogic_
ataucustom_
sesuatu benar-benar tidak begitu penting, saya pikir.Untuk mengomentari keduanya
stof
danbitset
:Kedua fungsi mengambil string sebagai argumen, dan dalam kedua kasus itu adalah:
Pernyataan ini memiliki, IMHO, dua akar:
Performa : Jika suatu fungsi dipanggil dalam jalur kritis, dan case "luar biasa" tidak luar biasa, yaitu sejumlah besar lintasan akan melibatkan melempar pengecualian, lalu membayar setiap waktu untuk pengecualian-mesin-gulungan tidak masuk akal , dan mungkin terlalu lambat.
Lokalitas penanganan kesalahan : Jika fungsi dipanggil dan pengecualian segera tertangkap dan diproses, maka ada gunanya melemparkan pengecualian, seperti penanganan kesalahan akan lebih verbose dengan yang
catch
dari denganif
.Contoh:
Di sinilah fungsi seperti
TryParse
vs.Parse
ikut bermain: Satu versi untuk saat kode lokal mengharapkan string yang diuraikan menjadi valid, satu versi ketika kode lokal mengasumsikan bahwa sebenarnya diharapkan (yaitu tidak luar biasa) bahwa parsing akan gagal.Memang,
stof
hanya (didefinisikan sebagai) pembungkusstrtof
, jadi jika Anda tidak ingin pengecualian, gunakan yang itu.IMHO, Anda memiliki dua kasus:
Fungsi "Perpustakaan" (sering digunakan kembali dalam konteks yang berbeda): Anda pada dasarnya tidak dapat memutuskan. Mungkin memberikan kedua versi, mungkin yang melaporkan kesalahan dan yang bungkus yang mengubah kesalahan yang dikembalikan menjadi pengecualian.
Fungsi "Aplikasi" (khusus untuk gumpalan kode aplikasi, dapat digunakan kembali beberapa, tetapi dibatasi oleh gaya penanganan kesalahan aplikasi, dll.): Di sini, harus sering dipotong cukup jelas. Jika jalur kode yang memanggil fungsi menangani pengecualian dengan cara yang waras dan bermanfaat, gunakan pengecualian untuk melaporkan kesalahan apa pun (tetapi lihat di bawah) . Jika kode aplikasi lebih mudah dibaca dan ditulis untuk gaya pengembalian kesalahan, tentu saja gunakan itu.
Tentu saja akan ada tempat di antara - gunakan saja apa yang perlu dan ingat YAGNI.
Terakhir, saya pikir saya harus kembali ke pernyataan FAQ,
Saya berlangganan ini untuk semua kesalahan yang merupakan indikasi jelas bahwa ada sesuatu yang sangat kacau atau kode panggilan jelas tidak tahu apa yang dilakukannya.
Tetapi ketika ini tepat sering aplikasi yang sangat spesifik, maka lihat domain perpustakaan di atas vs domain aplikasi.
Ini kembali pada pertanyaan apakah dan bagaimana memvalidasi prakondisi panggilan , tapi saya tidak akan membahasnya, jawabnya sudah terlalu lama :-)
sumber