Apa yang Anda anggap "praktik terbaik" ketika datang ke kesalahan penanganan kesalahan secara konsisten di perpustakaan C.
Ada dua cara yang saya pikirkan:
Selalu kembalikan kode kesalahan. Fungsi khas akan terlihat seperti ini:
MYAPI_ERROR getObjectSize(MYAPIHandle h, int* returnedSize);
Selalu memberikan pendekatan penunjuk kesalahan:
int getObjectSize(MYAPIHandle h, MYAPI_ERROR* returnedError);
Saat menggunakan pendekatan pertama dimungkinkan untuk menulis kode seperti ini di mana pemeriksaan penanganan kesalahan langsung ditempatkan pada pemanggilan fungsi:
int size;
if(getObjectSize(h, &size) != MYAPI_SUCCESS) {
// Error handling
}
Yang terlihat lebih baik daripada kode penanganan kesalahan di sini.
MYAPIError error;
int size;
size = getObjectSize(h, &error);
if(error != MYAPI_SUCCESS) {
// Error handling
}
Namun, saya pikir menggunakan nilai balik untuk mengembalikan data membuat kode lebih mudah dibaca, sudah jelas ada sesuatu yang ditulis ke variabel ukuran pada contoh kedua.
Apakah Anda punya ide mengapa saya lebih suka pendekatan itu atau mungkin menggabungkannya atau menggunakan yang lain? Saya bukan penggemar kondisi kesalahan global karena cenderung membuat penggunaan multi-threaded perpustakaan jauh lebih menyakitkan.
EDIT: C ++ ide spesifik tentang ini juga akan menarik untuk didengar selama mereka tidak melibatkan pengecualian karena ini bukan pilihan bagi saya saat ini ...
sumber
Jawaban:
Saya suka kesalahan sebagai cara nilai kembali. Jika Anda mendesain api dan ingin memanfaatkan perpustakaan Anda semudah mungkin, pikirkan tambahan ini:
simpan semua status kesalahan yang mungkin dalam satu enum yang diketikkan dan gunakan di lib Anda. Jangan hanya mengembalikan int atau bahkan lebih buruk, campur int atau enumerasi yang berbeda dengan kode-kembali.
menyediakan fungsi yang mengubah kesalahan menjadi sesuatu yang dapat dibaca manusia. Bisa sederhana. Hanya error-enum in, const char * out.
Saya tahu ide ini membuat penggunaan multithread sedikit sulit, tetapi alangkah baiknya jika programmer aplikasi dapat mengatur kesalahan-panggilan balik global. Dengan begitu mereka akan dapat membuat breakpoint ke dalam callback selama sesi pencarian bug.
Semoga ini bisa membantu.
sumber
Saya telah menggunakan kedua pendekatan, dan mereka berdua bekerja dengan baik untuk saya. Mana pun yang saya gunakan, saya selalu mencoba menerapkan prinsip ini:
Jika satu-satunya kesalahan yang mungkin adalah kesalahan programmer, jangan kembalikan kode kesalahan, gunakan menegaskan di dalam fungsi.
Pernyataan yang memvalidasi input dengan jelas mengkomunikasikan apa fungsi mengharapkan, sementara terlalu banyak pengecekan kesalahan dapat mengaburkan logika program. Memutuskan apa yang harus dilakukan untuk semua berbagai kasus kesalahan dapat benar-benar menyulitkan desain. Mengapa mencari tahu bagaimana functionX harus menangani pointer nol jika Anda bisa bersikeras bahwa programmer tidak pernah melewatkannya?
sumber
assert(X)
X adalah pernyataan C yang valid yang Anda inginkan benar. lihat stackoverflow.com/q/1571340/10396 .assert(X!=NULL);
atauassert(Y<enumtype_MAX);
? Lihat jawaban ini pada pemrogram dan pertanyaan yang ditautkan untuk lebih detail tentang mengapa saya pikir ini adalah cara yang tepat untuk pergi.Ada satu set slide yang bagus dari CU CMU dengan rekomendasi kapan harus menggunakan masing-masing teknik penanganan kesalahan C (dan C ++) yang umum. Salah satu slide terbaik adalah pohon keputusan ini:
Saya pribadi akan mengubah dua hal tentang diagram alur ini.
Pertama, saya akan menjelaskan bahwa kadang-kadang objek harus menggunakan nilai balik untuk menunjukkan kesalahan. Jika suatu fungsi hanya mengekstraksi data dari suatu objek tetapi tidak bermutasi objek, maka integritas objek itu sendiri tidak beresiko dan menunjukkan kesalahan menggunakan nilai kembali lebih tepat.
Kedua, tidak selalu tepat untuk menggunakan pengecualian dalam C ++. Pengecualian itu baik karena mereka dapat mengurangi jumlah kode sumber yang ditujukan untuk penanganan kesalahan, sebagian besar tidak memengaruhi tanda tangan fungsi, dan mereka sangat fleksibel dalam data apa yang dapat mereka lewatkan pada callstack. Di sisi lain, pengecualian mungkin bukan pilihan yang tepat karena beberapa alasan:
Pengecualian C ++ memiliki semantik yang sangat khusus. Jika Anda tidak menginginkan semantik tersebut, maka pengecualian C ++ adalah pilihan yang buruk. Pengecualian harus ditangani segera setelah dilempar dan desain mendukung kasus di mana kesalahan perlu melepaskan tumpukan panggilan beberapa tingkat.
Fungsi C ++ yang melempar pengecualian tidak dapat dibungkus kemudian untuk tidak melempar pengecualian, setidaknya tidak tanpa membayar biaya pengecualian penuh. Fungsi yang mengembalikan kode kesalahan dapat dibungkus untuk melempar pengecualian C ++, membuatnya lebih fleksibel. C ++
new
mendapatkan hak ini dengan menyediakan varian non-throwing.Pengecualian C ++ relatif mahal tetapi kelemahan ini sebagian besar berlebihan untuk program yang menggunakan pengecualian secara masuk akal. Suatu program tidak seharusnya membuang pengecualian pada codepath di mana kinerja menjadi perhatian. Tidak masalah seberapa cepat program Anda dapat melaporkan kesalahan dan keluar.
Terkadang pengecualian C ++ tidak tersedia. Entah itu benar-benar tidak tersedia dalam implementasi C ++ seseorang, atau pedoman kode seseorang melarangnya.
Karena pertanyaan awal adalah tentang konteks multithreaded, saya pikir teknik indikator kesalahan lokal (apa yang dijelaskan dalam SirDarius 's jawaban ) itu kurang dihargai dalam jawaban asli. Itu threadsafe, tidak memaksa kesalahan untuk segera ditangani oleh penelepon, dan dapat menggabungkan data acak yang menggambarkan kesalahan. The downside adalah bahwa ia harus dipegang oleh suatu objek (atau saya kira entah bagaimana terkait secara eksternal) dan bisa dibilang lebih mudah untuk diabaikan daripada kode kembali.
sumber
Saya menggunakan pendekatan pertama setiap kali saya membuat perpustakaan. Ada beberapa keuntungan menggunakan enum yang diketik sebagai kode pengembalian.
Jika fungsi mengembalikan output yang lebih rumit seperti array dan panjangnya, Anda tidak perlu membuat struktur arbitrer untuk kembali.
Ini memungkinkan penanganan kesalahan yang sederhana dan terstandarisasi.
Itu memungkinkan untuk penanganan kesalahan sederhana dalam fungsi perpustakaan.
Menggunakan enum typedef'ed juga memungkinkan nama enum terlihat di debugger. Ini memungkinkan untuk debugging lebih mudah tanpa perlu secara konstan berkonsultasi file header. Memiliki fungsi untuk menerjemahkan enum ini menjadi string juga membantu.
Masalah yang paling penting terlepas dari pendekatan yang digunakan adalah konsisten. Ini berlaku untuk penamaan fungsi dan argumen, urutan argumen dan penanganan kesalahan.
sumber
Gunakan setjmp .
http://en.wikipedia.org/wiki/Setjmp.h
http://aszt.inf.elte.hu/~gsd/halado_cpp/ch02s03.html
http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html
sumber
ETRY
kode telah direvisi sejak jawaban ini ditulis.setjmp
mahal, bahkan jika tidak ada kesalahan yang pernah dilemparkan, ia akan menghabiskan cukup banyak waktu dan ruang CPU. Saat menggunakan gcc untuk Windows, Anda dapat memilih antara berbagai metode penanganan pengecualian untuk C ++, salah satunya berdasarkansetjmp
dan itu membuat kode Anda hingga 30% lebih lambat dalam praktiknya.Saya pribadi lebih suka pendekatan sebelumnya (mengembalikan indikator kesalahan).
Bila perlu, hasil kembali hanya mengindikasikan bahwa kesalahan terjadi, dengan fungsi lain digunakan untuk mencari tahu kesalahan yang sebenarnya.
Dalam contoh getSize () Anda, saya akan mempertimbangkan bahwa ukuran harus selalu nol atau positif, jadi mengembalikan hasil negatif dapat menunjukkan kesalahan, seperti halnya panggilan sistem UNIX.
Saya tidak bisa memikirkan pustaka yang saya gunakan yang berlaku untuk pendekatan yang terakhir dengan objek kesalahan dilewatkan sebagai pointer.
stdio
, dll. semua berjalan dengan nilai pengembalian.sumber
Ketika saya menulis program, selama inisialisasi, saya biasanya memutar utas untuk penanganan kesalahan, dan menginisialisasi struktur khusus untuk kesalahan, termasuk kunci. Kemudian, ketika saya mendeteksi kesalahan, melalui nilai pengembalian, saya memasukkan info dari pengecualian ke dalam struktur dan mengirim SIGIO ke utas penanganan pengecualian, kemudian melihat apakah saya tidak dapat melanjutkan eksekusi. Jika saya tidak bisa, saya mengirim SIGURG ke utas pengecualian, yang menghentikan program dengan anggun.
sumber
Mengembalikan kode kesalahan adalah pendekatan yang biasa untuk penanganan kesalahan dalam C.
Namun baru-baru ini kami juga bereksperimen dengan pendekatan penunjuk kesalahan keluar.
Ini memiliki beberapa keunggulan dibandingkan pendekatan nilai pengembalian:
Anda dapat menggunakan nilai pengembalian untuk tujuan yang lebih bermakna.
Harus menulis bahwa parameter kesalahan mengingatkan Anda untuk menangani kesalahan atau menyebarkannya. (Anda tidak pernah lupa memeriksa nilai pengembalian
fclose
, bukan?)Jika Anda menggunakan pointer kesalahan, Anda bisa meneruskannya saat Anda memanggil fungsi. Jika ada fungsi yang mengaturnya, nilainya tidak akan hilang.
Dengan mengatur breakpoint data pada variabel kesalahan, Anda dapat mengetahui di mana kesalahan terjadi pertama kali. Dengan mengatur breakpoint bersyarat Anda juga dapat menangkap kesalahan tertentu.
Itu membuatnya lebih mudah untuk mengotomatisasi cek apakah Anda menangani semua kesalahan. Konvensi kode mungkin memaksa Anda untuk memanggil pointer kesalahan Anda
err
dan itu harus menjadi argumen terakhir. Jadi skrip bisa cocok dengan stringerr);
lalu periksa apakah diikutiif (*err
. Sebenarnya dalam praktiknya kami membuat makro bernamaCER
(check err return) danCEG
(check err goto). Jadi, Anda tidak perlu mengetiknya selalu saat kami hanya ingin mengembalikan kesalahan, dan dapat mengurangi kekacauan visual.Namun, tidak semua fungsi dalam kode kami memiliki parameter keluar ini. Parameter parameter keluar ini digunakan untuk kasus-kasus di mana Anda biasanya melempar pengecualian.
sumber
Saya telah melakukan banyak pemrograman C di masa lalu. Dan saya sangat menghargai nilai pengembalian kode kesalahan. Tetapi ada beberapa kemungkinan jebakan:
sumber
Pendekatan UNIX paling mirip dengan saran kedua Anda. Kembalikan nilai hasil atau nilai "it go wrong". Misalnya, terbuka akan mengembalikan deskriptor file jika berhasil atau -1 pada kegagalan. Pada kegagalan itu juga menetapkan
errno
, integer global eksternal untuk menunjukkan kegagalan mana yang terjadi.Untuk apa nilainya, Kakao juga telah mengadopsi pendekatan serupa. Sejumlah metode mengembalikan BOOL, dan mengambil
NSError **
parameter, sehingga pada kegagalan mereka mengatur kesalahan dan mengembalikan NO. Maka penanganan kesalahan terlihat seperti:yang berada di antara dua opsi Anda :-).
sumber
Saya merenungkan masalah ini baru-baru ini juga, dan menulis beberapa makro untuk C yang mensimulasikan semantik try-catch-akhirnya menggunakan nilai pengembalian murni lokal. Semoga bermanfaat.
sumber
Ini adalah pendekatan yang menurut saya menarik, sambil membutuhkan disiplin.
Ini mengasumsikan variabel tipe pegangan adalah turunan yang mengoperasikan semua fungsi API.
Idenya adalah bahwa struct di belakang gagang menyimpan kesalahan sebelumnya sebagai struct dengan data yang diperlukan (kode, pesan ...), dan pengguna diberikan fungsi yang mengembalikan pointer ke objek kesalahan ini. Setiap operasi akan memperbarui objek runcing sehingga pengguna dapat memeriksa statusnya tanpa fungsi panggilan. Berbeda dengan pola errno, kode kesalahan tidak bersifat global, yang membuat pendekatan thread-safe, selama masing-masing pegangan digunakan dengan benar.
Contoh:
sumber
Pendekatan pertama adalah IMHO yang lebih baik:
sumber
Saya pasti lebih suka solusi pertama:
saya akan sedikit memodifikasinya, untuk:
Selain itu saya tidak akan pernah mencampur nilai pengembalian yang sah dengan kesalahan bahkan jika saat ini ruang lingkup fungsi memungkinkan Anda untuk melakukannya, Anda tidak pernah tahu ke mana implementasi fungsi akan berjalan di masa depan.
Dan jika kita sudah berbicara tentang penanganan kesalahan saya sarankan
goto Error;
sebagai kode penanganan kesalahan, kecuali beberapaundo
fungsi dapat dipanggil untuk menangani penanganan kesalahan dengan benar.sumber
Apa yang bisa Anda lakukan alih-alih mengembalikan kesalahan Anda, dan dengan demikian melarang Anda mengembalikan data dengan fungsi Anda, menggunakan pembungkus untuk jenis pengembalian Anda:
Kemudian, dalam fungsi yang disebut:
Harap dicatat bahwa dengan metode berikut, pembungkus akan memiliki ukuran MyType plus satu byte (pada kebanyakan kompiler), yang cukup menguntungkan; dan Anda tidak perlu mendorong argumen lain pada stack ketika Anda memanggil fungsi Anda (
returnedSize
ataureturnedError
dalam kedua metode yang Anda sajikan).sumber
Berikut adalah program sederhana untuk mendemonstrasikan 2 butir pertama jawaban Nils Pipenbrinck di sini .
2 peluru pertamanya adalah:
Asumsikan Anda telah menulis modul bernama
mymodule
. Pertama, di mymodule.h, Anda mendefinisikan kode kesalahan berbasis enum, dan Anda menulis beberapa string kesalahan yang sesuai dengan kode ini. Di sini saya menggunakan array string C (char *
), yang hanya berfungsi dengan baik jika kode kesalahan berbasis enum pertama Anda memiliki nilai 0, dan Anda tidak memanipulasi angka setelahnya. Jika Anda menggunakan nomor kode kesalahan dengan celah atau nilai awal lainnya, Anda hanya perlu mengubah dari menggunakan array C-string yang dipetakan (seperti yang saya lakukan di bawah) menjadi menggunakan fungsi yang menggunakan pernyataan switch atau jika / selain itu jika pernyataan untuk memetakan dari kode kesalahan enum ke string C dicetak (yang saya tidak menunjukkan). Pilihan ada padamu.mymodule.h
mymodule.c berisi fungsi pemetaan saya untuk memetakan dari kode kesalahan enum ke string C yang dapat dicetak:
mymodule.c
main.c berisi program pengujian untuk mendemonstrasikan memanggil beberapa fungsi dan mencetak beberapa kode kesalahan dari mereka:
main.c
Keluaran:
Referensi:
Anda dapat menjalankan kode ini sendiri di sini: https://onlinegdb.com/ByEbKLupS .
sumber
Selain apa yang telah dikatakan, sebelum mengembalikan kode kesalahan Anda, jalankan penegasan atau diagnostik serupa ketika kesalahan dikembalikan, karena akan membuat pelacakan jauh lebih mudah. Cara saya melakukan ini adalah memiliki pernyataan khusus yang masih dapat dikompilasi pada saat rilis tetapi hanya dipecat ketika perangkat lunak dalam mode diagnostik, dengan opsi untuk secara diam-diam melaporkan ke file log atau berhenti di layar.
Saya pribadi mengembalikan kode kesalahan sebagai bilangan bulat negatif dengan no_error sebagai nol, tetapi tidak meninggalkan Anda dengan kemungkinan bug berikut
Alternatif adalah kegagalan selalu dikembalikan sebagai nol, dan gunakan fungsi LastError () untuk memberikan rincian kesalahan aktual.
sumber
Saya bertemu dengan Tanya Jawab ini beberapa kali, dan ingin berkontribusi jawaban yang lebih komprehensif. Saya pikir cara terbaik untuk berpikir tentang ini adalah bagaimana mengembalikan kesalahan kepada penelepon, dan apa yang Anda kembali.
Bagaimana
Ada 3 cara untuk mengembalikan informasi dari suatu fungsi:
Nilai Pengembalian
Anda hanya dapat mengembalikan nilai sebagai objek tunggal, namun, itu bisa berupa kompleks arbitrer. Berikut adalah contoh fungsi pengembalian kesalahan:
Salah satu manfaat dari nilai-nilai pengembalian adalah memungkinkan chaining panggilan untuk penanganan kesalahan yang tidak terlalu mengganggu:
Ini bukan hanya tentang keterbacaan, tetapi juga memungkinkan pemrosesan array dari pointer fungsi tersebut dengan cara yang seragam.
Argumen Keluar
Anda dapat mengembalikan lebih dari satu objek melalui argumen, tetapi praktik terbaik menyarankan agar jumlah argumen tetap rendah (katakanlah, <= 4):
Keluar dari Band
Dengan setjmp () Anda menentukan tempat dan bagaimana Anda ingin menangani nilai int, dan Anda mentransfer kontrol ke lokasi itu melalui longjmp (). Lihat penggunaan praktis setjmp dan longjmp di C .
Apa
Indikator
Indikator kesalahan hanya memberitahu Anda bahwa ada masalah tetapi tidak ada tentang sifat dari masalah tersebut:
Ini adalah cara yang paling tidak ampuh untuk fungsi untuk mengkomunikasikan keadaan kesalahan, bagaimanapun, sempurna jika penelepon tidak dapat menanggapi kesalahan dengan cara apa pun.
Kode
Kode kesalahan memberi tahu penelepon tentang sifat masalah, dan memungkinkan respons yang sesuai (dari atas). Ini bisa berupa nilai balik, atau seperti contoh look_ma () di atas argumen kesalahan.
Obyek
Dengan objek kesalahan, penelepon dapat diberitahu tentang masalah rumit yang sewenang-wenang. Misalnya, kode kesalahan dan pesan yang dapat dibaca manusia yang sesuai. Ini juga dapat memberi tahu penelepon bahwa banyak hal yang salah, atau kesalahan per item saat memproses koleksi:
Alih-alih pra-alokasi array kesalahan, Anda juga dapat (kembali) mengalokasikannya secara dinamis sesuai kebutuhan tentu saja.
Telepon balik
Callback adalah cara paling ampuh untuk menangani kesalahan, karena Anda bisa memberi tahu fungsi itu perilaku apa yang ingin Anda lihat terjadi ketika terjadi kesalahan. Argumen panggilan balik dapat ditambahkan ke setiap fungsi, atau jika kustomisasi uis hanya diperlukan per instance dari struct seperti ini:
Salah satu manfaat menarik dari callback adalah dapat dipanggil beberapa kali, atau tidak ada sama sekali karena tidak ada kesalahan di mana tidak ada overhead di jalur bahagia.
Namun, ada inversi kontrol. Kode panggilan tidak tahu apakah panggilan balik itu dipanggil. Karena itu, masuk akal juga menggunakan indikator.
sumber
EDIT: Jika Anda hanya perlu mengakses kesalahan terakhir, dan Anda tidak bekerja di lingkungan multithreaded.
Anda hanya dapat mengembalikan true / false (atau semacam #define jika Anda bekerja di C dan tidak mendukung variabel bool), dan memiliki penyangga Galat global yang akan menampung kesalahan terakhir:
sumber
Pendekatan kedua memungkinkan kompiler menghasilkan kode yang lebih optimal, karena ketika alamat variabel diteruskan ke suatu fungsi, kompiler tidak dapat menyimpan nilainya dalam register (s) selama panggilan berikutnya ke fungsi lain. Kode penyelesaian biasanya digunakan hanya sekali, tepat setelah panggilan, sedangkan data "nyata" yang dikembalikan dari panggilan dapat digunakan lebih sering
sumber
Saya lebih suka penanganan kesalahan dalam C menggunakan teknik berikut:
Sumber: http://blog.staila.com/?p=114
sumber
goto
itu, bukan diulangif
. Referensi: satu , dua .Selain jawaban hebat lainnya, saya sarankan Anda mencoba memisahkan tanda kesalahan dan kode kesalahan untuk menyimpan satu baris pada setiap panggilan, yaitu:
Ketika Anda memiliki banyak pengecekan kesalahan, penyederhanaan kecil ini sangat membantu.
sumber