Kesalahan dalam menangani kode C.

152

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

Laserallan
sumber
Saya baru belajar C selama sekitar dua minggu, tetapi yang saya rasakan adalah bahwa parameter OUT adalah nilai pengembalian defacto untuk sebagian besar fungsi, karena ia menghindari overhead pengembalian struct oleh-nilai dan mitigasi kebutuhan untuk membatalkan alokasi memori karena sebagian besar variabel ada di stack. Jadi karena saya tidak menggunakan "kembali" untuk nilai fungsi sebenarnya, saya bebas menggunakannya untuk penanganan kesalahan sebagian besar waktu.
Joel Roberts

Jawaban:

74

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.

Nils Pipenbrinck
sumber
5
Mengapa Anda berkata, "ide ini membuat penggunaan multi-utas sedikit sulit." Bagian mana yang dipersulit oleh multi-threading? Bisakah Anda memberi contoh cepat?
SayeedHussain
1
@crypticcoder Sederhananya: panggilan balik kesalahan global dapat dipanggil dalam konteks utas apa pun. Jika Anda baru saja mencetak kesalahan Anda tidak akan menghadapi masalah. Jika Anda mencoba untuk memperbaiki masalah, Anda harus mengetahui utas panggilan mana yang menyebabkan kesalahan, dan itu membuat segalanya menjadi sulit.
Nils Pipenbrinck
9
Bagaimana jika Anda ingin mengomunikasikan rincian kesalahan lebih lanjut? Misalnya Anda memiliki kesalahan parser dan ingin memberikan nomor baris dan kolom kesalahan sintaks dan cara untuk mencetak semuanya dengan baik.
panzi
1
@panzi maka Anda jelas perlu mengembalikan struct (atau menggunakan pointer keluar jika struct benar-benar besar) dan memiliki fungsi untuk memformat struct sebagai string.
Sayap Sendon
Saya mendemonstrasikan 2 peluru pertama Anda dalam kode di sini: stackoverflow.com/questions/385975/error-handling-in-c-code/…
Gabriel Staples
92

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?

ASHelly
sumber
1
Punya contoh pernyataan di C? (Saya sangat hijau untuk C)
thomthom
Biasanya sesederhana assert(X)X adalah pernyataan C yang valid yang Anda inginkan benar. lihat stackoverflow.com/q/1571340/10396 .
AShelly
14
Ugh, sama sekali tidak pernah menggunakan pernyataan dalam kode perpustakaan ! Juga, jangan campur berbagai gaya penanganan kesalahan dalam satu kode seperti yang lainnya ...
mirabilos
10
Saya tentu saja setuju untuk tidak mencampur gaya. Saya ingin tahu tentang alasan Anda pada pernyataan. Jika dokumentasi fungsi saya mengatakan "argumen X tidak boleh NULL" atau "Y harus menjadi anggota enum ini", daripada apa yang salah dengan assert(X!=NULL);atau assert(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.
AShelly
8
@ Ashelly Masalah dengan menegaskan bahwa mereka biasanya tidak ada dalam rilis build.
Calmarius
29

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:

Kesalahan Menangani Pohon Keputusan

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:

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

  2. 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 ++ newmendapatkan hak ini dengan menyediakan varian non-throwing.

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

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

Praxeolitic
sumber
5
Anda mungkin memperhatikan bahwa standar pengkodean C ++ Google masih mengatakan Kami tidak menggunakan pengecualian C ++.
Jonathan Leffler
19

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.

    rc = func(..., int **return_array, size_t *array_length);
  • Ini memungkinkan penanganan kesalahan yang sederhana dan terstandarisasi.

    if ((rc = func(...)) != API_SUCCESS) {
       /* Error Handling */
    }
  • Itu memungkinkan untuk penanganan kesalahan sederhana dalam fungsi perpustakaan.

    /* Check for valid arguments */
    if (NULL == return_array || NULL == array_length)
        return API_INVALID_ARGS;
  • 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.

Jeffrey Cohen
sumber
9

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

#include <setjmp.h>
#include <stdio.h>

jmp_buf x;

void f()
{
    longjmp(x,5); // throw 5;
}

int main()
{
    // output of this program is 5.

    int i = 0;

    if ( (i = setjmp(x)) == 0 )// try{
    {
        f();
    } // } --> end of try{
    else // catch(i){
    {
        switch( i )
        {
        case  1:
        case  2:
        default: fprintf( stdout, "error code = %d\n", i); break;
        }
    } // } --> end of catch(i){
    return 0;
}

#include <stdio.h>
#include <setjmp.h>

#define TRY do{ jmp_buf ex_buf__; if( !setjmp(ex_buf__) ){
#define CATCH } else {
#define ETRY } }while(0)
#define THROW longjmp(ex_buf__, 1)

int
main(int argc, char** argv)
{
   TRY
   {
      printf("In Try Statement\n");
      THROW;
      printf("I do not appear\n");
   }
   CATCH
   {
      printf("Got Exception!\n");
   }
   ETRY;

   return 0;
}
Amir Saniyan
sumber
2
Blok kode kedua didasarkan pada versi kode yang lebih lama di halaman Francesco Nidito yang dirujuk di bagian atas jawabannya. The ETRYkode telah direvisi sejak jawaban ini ditulis.
Jonathan Leffler
2
Setjmp adalah strategi penanganan kesalahan yang mengerikan. Itu mahal, rawan kesalahan (dengan non-volatil yang diubah penduduk setempat tidak mempertahankan nilai yang diubah dan semuanya) dan membocorkan sumber daya jika Anda mengalokasikan apa pun di antara panggilan setjmp dan longjmp. Anda harus dapat melakukan seperti 30 pengembalian dan pemeriksaan int-val sebelum Anda mengganti biaya sigjmp / longjmp. Kebanyakan callstacks tidak masuk sedalam itu terutama jika Anda tidak melakukan rekursi (dan jika Anda melakukannya, Anda memiliki masalah perf selain biaya pengembalian + cek).
PSkocik
1
Jika Anda menyimpan memori lalu membuang, memori akan bocor selamanya. Juga setjmpmahal, 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 berdasarkan setjmpdan itu membuat kode Anda hingga 30% lebih lambat dalam praktiknya.
Mecki
7

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.

Alnitak
sumber
1
Sebagai catatan, satu perpustakaan saya telah melihat menggunakan pendekatan yang terakhir adalah API pemrograman Maya. Ini adalah pustaka c ++ daripada C sekalipun. Ini cukup tidak konsisten dalam cara menangani kesalahannya dan kadang-kadang kesalahan dilewatkan sebagai nilai balik dan lain kali melewati hasil sebagai referensi.
Laserallan
1
jangan lupa strtod, ok, argumen terakhir tidak hanya untuk menunjukkan kesalahan, tetapi juga melakukannya.
quinmars
7

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.

Henry
sumber
7

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 errdan itu harus menjadi argumen terakhir. Jadi skrip bisa cocok dengan string err);lalu periksa apakah diikuti if (*err. Sebenarnya dalam praktiknya kami membuat makro bernama CER(check err return) dan CEG(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.

Calmarius
sumber
6

Saya telah melakukan banyak pemrograman C di masa lalu. Dan saya sangat menghargai nilai pengembalian kode kesalahan. Tetapi ada beberapa kemungkinan jebakan:

  • Nomor kesalahan duplikat, ini dapat diselesaikan dengan file global errors.h.
  • Lupa memeriksa kode kesalahan, ini harus diselesaikan dengan cluebat dan waktu debug yang panjang. Tetapi pada akhirnya Anda akan belajar (atau Anda akan tahu bahwa orang lain akan melakukan debugging).
Toon Krijthe
sumber
2
masalah kedua dapat diselesaikan dengan tingkat peringatan kompiler yang tepat, mekanisme peninjauan kode yang tepat dan oleh alat penganalisa kode statis.
Ilya
1
Anda juga dapat bekerja pada prinsip: jika fungsi API dipanggil dan nilai kembali tidak dicentang, ada bug.
Jonathan Leffler
6

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:

NSError *error = nil;
if ([myThing doThingError: &error] == NO)
{
  // error handling
}

yang berada di antara dua opsi Anda :-).


sumber
5

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:

MyHandle * h = MyApiCreateHandle();

/* first call checks for pointer nullity, since we cannot retrieve error code
   on a NULL pointer */
if (h == NULL)
     return 0; 

/* from here h is a valid handle */

/* get a pointer to the error struct that will be updated with each call */
MyApiError * err = MyApiGetError(h);


MyApiFileDescriptor * fd = MyApiOpenFile("/path/to/file.ext");

/* we want to know what can go wrong */
if (err->code != MyApi_ERROR_OK) {
    fprintf(stderr, "(%d) %s\n", err->code, err->message);
    MyApiDestroy(h);
    return 0;
}

MyApiRecord record;

/* here the API could refuse to execute the operation if the previous one
   yielded an error, and eventually close the file descriptor itself if
   the error is not recoverable */
MyApiReadFileRecord(h, &record, sizeof(record));

/* we want to know what can go wrong, here using a macro checking for failure */
if (MyApi_FAILED(err)) {
    fprintf(stderr, "(%d) %s\n", err->code, err->message);
    MyApiDestroy(h);
    return 0;
}
SirDarius
sumber
4

Pendekatan pertama adalah IMHO yang lebih baik:

  • Lebih mudah untuk menulis fungsi seperti itu. Ketika Anda melihat kesalahan di tengah fungsi Anda hanya mengembalikan nilai kesalahan. Dalam pendekatan kedua Anda perlu menetapkan nilai kesalahan ke salah satu parameter dan kemudian mengembalikan sesuatu .... tapi apa yang akan Anda kembali - Anda tidak memiliki nilai yang benar dan Anda tidak mengembalikan nilai kesalahan.
  • itu lebih populer sehingga akan lebih mudah dipahami, dipelihara
Klesk
sumber
4

Saya pasti lebih suka solusi pertama:

int size;
if(getObjectSize(h, &size) != MYAPI_SUCCESS) {
  // Error handling
}

saya akan sedikit memodifikasinya, untuk:

int size;
MYAPIError rc;

rc = getObjectSize(h, &size)
if ( rc != MYAPI_SUCCESS) {
  // Error handling
}

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 beberapa undofungsi dapat dipanggil untuk menangani penanganan kesalahan dengan benar.

Ilya
sumber
3

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:

typedef struct {
    enum {SUCCESS, ERROR} status;
    union {
        int errCode;
        MyType value;
    } ret;
} MyTypeWrapper;

Kemudian, dalam fungsi yang disebut:

MyTypeWrapper MYAPIFunction(MYAPIHandle h) {
    MyTypeWrapper wrapper;
    // [...]
    // If there is an error somewhere:
    wrapper.status = ERROR;
    wrapper.ret.errCode = MY_ERROR_CODE;

    // Everything went well:
    wrapper.status = SUCCESS;
    wrapper.ret.value = myProcessedData;
    return wrapper;
} 

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 ( returnedSizeatau returnedErrordalam kedua metode yang Anda sajikan).

Antoine C.
sumber
3

Berikut adalah program sederhana untuk mendemonstrasikan 2 butir pertama jawaban Nils Pipenbrinck di sini .

2 peluru pertamanya adalah:

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

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

/// @brief Error codes for library "mymodule"
typedef enum mymodule_error_e
{
    /// No error
    MYMODULE_ERROR_OK = 0,
    
    /// Invalid arguments (ex: NULL pointer where a valid pointer is required)
    MYMODULE_ERROR_INVARG,

    /// Out of memory (RAM)
    MYMODULE_ERROR_NOMEM,

    /// Make up your error codes as you see fit
    MYMODULE_ERROR_MYERROR, 

    // etc etc
    
    /// Total # of errors in this list (NOT AN ACTUAL ERROR CODE);
    /// NOTE: that for this to work, it assumes your first error code is value 0 and you let it naturally 
    /// increment from there, as is done above, without explicitly altering any error values above
    MYMODULE_ERROR_COUNT,
} mymodule_error_t;

// Array of strings to map enum error types to printable strings
// - see important NOTE above!
const char* const MYMODULE_ERROR_STRS[] = 
{
    "MYMODULE_ERROR_OK",
    "MYMODULE_ERROR_INVARG",
    "MYMODULE_ERROR_NOMEM",
    "MYMODULE_ERROR_MYERROR",
};

// To get a printable error string
const char* mymodule_error_str(mymodule_error_t err);

// Other functions in mymodule
mymodule_error_t mymodule_func1(void);
mymodule_error_t mymodule_func2(void);
mymodule_error_t mymodule_func3(void);

mymodule.c berisi fungsi pemetaan saya untuk memetakan dari kode kesalahan enum ke string C yang dapat dicetak:

mymodule.c

#include <stdio.h>

/// @brief      Function to get a printable string from an enum error type
/// @param[in]  err     a valid error code for this module
/// @return     A printable C string corresponding to the error code input above, or NULL if an invalid error code
///             was passed in
const char* mymodule_error_str(mymodule_error_t err)
{
    const char* err_str = NULL;

    // Ensure error codes are within the valid array index range
    if (err >= MYMODULE_ERROR_COUNT)
    {
        goto done;
    }

    err_str = MYMODULE_ERROR_STRS[err];

done:
    return err_str;
}

// Let's just make some empty dummy functions to return some errors; fill these in as appropriate for your 
// library module

mymodule_error_t mymodule_func1(void)
{
    return MYMODULE_ERROR_OK;
}

mymodule_error_t mymodule_func2(void)
{
    return MYMODULE_ERROR_INVARG;
}

mymodule_error_t mymodule_func3(void)
{
    return MYMODULE_ERROR_MYERROR;
}

main.c berisi program pengujian untuk mendemonstrasikan memanggil beberapa fungsi dan mencetak beberapa kode kesalahan dari mereka:

main.c

#include <stdio.h>

int main()
{
    printf("Demonstration of enum-based error codes in C (or C++)\n");

    printf("err code from mymodule_func1() = %s\n", mymodule_error_str(mymodule_func1()));
    printf("err code from mymodule_func2() = %s\n", mymodule_error_str(mymodule_func2()));
    printf("err code from mymodule_func3() = %s\n", mymodule_error_str(mymodule_func3()));

    return 0;
}

Keluaran:

Demonstrasi kode kesalahan berbasis enum dalam C (atau C ++)
kode err dari mymodule_func1 () = MYMODULE_ERROR_OK
kode err dari mymodule_func2 () = MYMODULE_ERROR_INVARG
kode err dari mymodule_func3 () = MYMODRER_RRRR_RRM_RR_RR_RRM_RRM

Referensi:

Anda dapat menjalankan kode ini sendiri di sini: https://onlinegdb.com/ByEbKLupS .

Gabriel Staples
sumber
2

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

if (MyFunc())
 DoSomething();

Alternatif adalah kegagalan selalu dikembalikan sebagai nol, dan gunakan fungsi LastError () untuk memberikan rincian kesalahan aktual.

SmacL
sumber
2

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:

  1. Nilai Pengembalian
  2. Argumen Keluar
  3. Out of Band, yang mencakup goto non-lokal (setjmp / longjmp), file atau variabel cakupan global, sistem file, dll.

Nilai Pengembalian

Anda hanya dapat mengembalikan nilai sebagai objek tunggal, namun, itu bisa berupa kompleks arbitrer. Berikut adalah contoh fungsi pengembalian kesalahan:

  enum error hold_my_beer();

Salah satu manfaat dari nilai-nilai pengembalian adalah memungkinkan chaining panggilan untuk penanganan kesalahan yang tidak terlalu mengganggu:

  !hold_my_beer() &&
  !hold_my_cigarette() &&
  !hold_my_pants() ||
  abort();

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):

void look_ma(enum error *e, char *what_broke);

enum error e;
look_ma(e);
if(e == FURNITURE) {
  reorder(what_broke);
} else if(e == SELF) {
  tell_doctor(what_broke);
}

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

  1. Indikator
  2. Kode
  3. Obyek
  4. Telepon balik

Indikator

Indikator kesalahan hanya memberitahu Anda bahwa ada masalah tetapi tidak ada tentang sifat dari masalah tersebut:

struct foo *f = foo_init();
if(!f) {
  /// handle the absence of foo
}

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:

struct collection friends;
enum error *e = malloc(c.size * sizeof(enum error));
...
ask_for_favor(friends, reason);
for(int i = 0; i < c.size; i++) {
   if(reason[i] == NOT_FOUND) find(friends[i]);
}

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:

 struct foo {
    ...
    void (error_handler)(char *);
 };

 void default_error_handler(char *message) { 
    assert(f);
    printf("%s", message);
 }

 void foo_set_error_handler(struct foo *f, void (*eh)(char *)) {
    assert(f);
    f->error_handler = eh;
 }

 struct foo *foo_init() {
    struct foo *f = malloc(sizeof(struct foo));
    foo_set_error_handler(f, default_error_handler);
    return f;
 }


 struct foo *f = foo_init();
 foo_something();

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.

Allan Wind
sumber
1

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:

int getObjectSize(MYAPIHandle h, int* returnedSize);
MYAPI_ERROR LastError;
MYAPI_ERROR* getLastError() {return LastError;};
#define FUNC_SUCCESS 1
#define FUNC_FAIL 0

if(getObjectSize(h, &size) != FUNC_SUCCESS ) {
    MYAPI_ERROR* error = getLastError();
    // error handling
}
Igor Oks
sumber
Memang tapi itu bukan C itu mungkin disediakan oleh OS atau tidak. Jika Anda bekerja pada sistem operasi waktu nyata misalnya Anda mungkin tidak memilikinya.
Ilya
1

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

dmityugov
sumber
1

Saya lebih suka penanganan kesalahan dalam C menggunakan teknik berikut:

struct lnode *insert(char *data, int len, struct lnode *list) {
    struct lnode *p, *q;
    uint8_t good;
    struct {
            uint8_t alloc_node : 1;
            uint8_t alloc_str : 1;
    } cleanup = { 0, 0 };

   // allocate node.
    p = (struct lnode *)malloc(sizeof(struct lnode));
    good = cleanup.alloc_node = (p != NULL);

   // good? then allocate str
    if (good) {
            p->str = (char *)malloc(sizeof(char)*len);
            good = cleanup.alloc_str = (p->str != NULL);
    }

   // good? copy data
    if(good) {
            memcpy ( p->str, data, len );
    }

   // still good? insert in list
    if(good) {
            if(NULL == list) {
                    p->next = NULL;
                    list = p;
            } else {
                    q = list;
                    while(q->next != NULL && good) {
                            // duplicate found--not good
                            good = (strcmp(q->str,p->str) != 0);
                            q = q->next;
                    }
                    if (good) {
                            p->next = q->next;
                            q->next = p;
                    }
            }
    }

   // not-good? cleanup.
    if(!good) {
            if(cleanup.alloc_str)   free(p->str);
            if(cleanup.alloc_node)  free(p);
    }

   // good? return list or else return NULL
    return (good ? list : NULL);
}

Sumber: http://blog.staila.com/?p=114

Nitin Kunal
sumber
1
Teknik yang baik. Saya menemukan bahkan lebih rapi dengan gotoitu, bukan diulang if. Referensi: satu , dua .
Ant_222
0

Selain jawaban hebat lainnya, saya sarankan Anda mencoba memisahkan tanda kesalahan dan kode kesalahan untuk menyimpan satu baris pada setiap panggilan, yaitu:

if( !doit(a, b, c, &errcode) )
{   (* handle *)
    (* thine  *)
    (* error  *)
}

Ketika Anda memiliki banyak pengecekan kesalahan, penyederhanaan kecil ini sangat membantu.

Ant_222
sumber