Любые хорошие идиомы для обработки ошибок в прямых программах на C?

Возrotation к некоторым работам C.

Многие из моих функций выглядят так:

int err = do_something(arg1, arg2, arg3, &result); 

С намерением результат заносится в функцию, а возвращаемое значение – это статус вызова.

Темная сторона – вы получаете что-то наивное, как это:

 int err = func1(...); if (!err) { err = func2(...); if (!err) { err = func3(...); } } return err; 

Я мог бы использовать макрос, я полагаю:

 #define ERR(x) if (!err) { err = (x) } int err = 0; ERR(func1(...)); ERR(func2(...)); ERR(func3(...)); return err; 

Но это работает только в том случае, если я связываю вызовы функций, а делаю другую работу.

Очевидно, что Java, C #, C ++ имеют исключения, которые очень хорошо работают для таких вещей.

Мне просто интересно, что делают другие люди и как другие люди справляются с ошибками в своих программах на C в наши дни.

Два типичных шаблона:

 int major_func() { int err = 0; if (err = minor_func1()) return err; if (err = minor_func2()) return err; if (err = minor_func3()) return err; return 0; } int other_idea() { int err = minor_func1(); if (!err) err = minor_func2(); if (!err) err = minor_func3(); return err; } void main_func() { int err = major_func(); if (err) { show_err(); return; } happy_happy_joy_joy(); err = other_idea(); if (err) { show_err(); return; } happy_happy_joy_joy(); } 

Если у вас есть ресурсы, которые должны быть выпущены в конце, то иногда старый надежный goto может быть удобен!

 int major_func(size_t len) { int err; char *buf; buf = malloc(len); if (err = minor_func1(buf)) goto major_func_end; if (err = minor_func2(buf)) goto major_func_end; if (err = minor_func3(buf)) goto major_func_end; major_func_end: free(buf); return err; } 

Что вы делаете в операторах else ? Если ничего, попробуйте следующее:

 int err = func1(...); if (err) { return err; } err = func2(...); if (err) { return err; } err = func3(...); return err; 

Таким образом, вы замыкаете всю функцию, даже не беспокоясь о следующих вызовах функций.

РЕДАКТИРОВАТЬ

Возвращаясь назад и снова читаю, я понимаю, что неважно, что вы делаете в своих else заявлениях. Этот код может легко перейти сразу после блоков if .

Если коды ошибок являются логическими, попробуйте более простой код:

 return func1() && func2() && func3() 

Один из подходов, который был предпринят OpenGL, заключается в том, чтобы не возвращать ошибки из функций вообще, а скорее представлять состояние ошибки, которое может быть проверено после вызова функции. Одна из приятных вещей в этом подходе заключается в том, что, когда у вас есть функция, на которую вы действительно хотите вернуть что-то иное, кроме кода ошибки, вы можете обрабатывать ошибки одинаково. Еще одна вещь, которая хороша в этом, заключается в том, что если пользователь хочет вызвать несколько функций и добиться успеха, только если все они были успешными, вы можете проверить наличие ошибок после количества вызовов.

 /* call a number of functions which may error.. */ glMatrixMode(GL_MODELVIEW); glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_TEXTURE_COORD_ARRAY); glEnable(GL_TEXTURE_2D); /* ...check for errors */ if ((error = glGetError()) != GL_NO_ERROR) { if (error == GL_INVALID_VALUE) printf("error: invalid value creating view"); else if (error == GL_INVALID_OPERATION) printf("error: invalid operation creating view"); else if (error == GL_OUT_OF_MEMORY) printf("error: out of memory creating view"); } 

Другие предложили хорошие идеи. Вот идиомы, которые я видел

 int err; ... err = foo(...); if (err) return err; ... 

Вы можете сделать макрос таким, как

 #define dERR int err=0 #define CALL err = #define CHECK do { if (err) return err } while(0) ... void my_func(void) { dERR; ... CALL foo(...); CHECK; 

или, если вы чувствуете себя действительно мотивированным, играйте с CALL и CHECK, чтобы они могли использоваться как

 CALL foo(...) CHECK; 

или же

 CALL( foo(...) ); 

Часто функции, которые должны выполнять очистку при выходе (например, свободная память), записываются так:

 int do_something_complicated(...) { ... err = first_thing(); if (err) goto err_out; buffer = malloc(...); if (buffer == NULL) goto err_out err = another_complicated(...); if (err) goto err_out_free; ... err_out_free: free(buffer); err_out: return err; /* err might be zero */ } 

Вы можете использовать этот шаблон или попытаться упростить его с помощью макросов.

Наконец, если вы чувствуете / действительно / мотивированы, вы можете использовать setjmp / longjmp.

 int main(int argc, char *argv[]) { jmp_buf on_error; int err; if (err = setjmp(on_error)) { /* error occurred, error code in err */ return 1; } else { actual_code(..., on_error); return 0; } } void actual_code(..., jmp_buf on_error) { ... if (err) longjmp(on_error, err); } 

По существу, объявление нового jmp_buf и функции setjmp как настройка блока try. Случай, когда setjmp возвращает ненулевое значение, является вашим catch, а вызов longjmp – ваш бросок. Я написал это с передачей jmp_buf на случай, если вы хотите вложенных обработчиков (например, если вам нужно освободить материал, прежде чем сигнализировать об ошибке); если вам это не нужно, не стесняйтесь объявлять err и jmp_buf как глобальные.

В качестве альтернативы вы можете использовать macros для простого прохождения аргумента. Я бы предложил, как это делает реализация Perl:

 #define pERR jmp_buf _err_handler #define aERR _err_handler #define HANDLE_ERRORS do { jmp_buf _err_handler; int err = setjmp(_err_handler); #define END_HANDLE while(0) #define TRY if (! err) #define CATCH else #define THROW(e) longjmp(_err_handler, e) void always_fails(pERR, int other_arg) { THROW(42); } void does_some_stuff(pERR) { normal_call(aERR); HANDLE_ERRORS TRY { always_fails(aERR, 23); } CATCH { /* err is 42 */ } END_HANDLE; } int main(int argc, char *argv[]) { HANDLE_ERRORS TRY { does_some_stuff(aERR); return 0; } CATCH { return err; } DONE_ERRORS; } 

Уф. Я задолбался. (Сумасшедшие примеры непроверены. Некоторые детали могут быть отключены.)

Вы должны проверить, что DirectX сделал с HRESULT – это в основном это. Есть причина, что исключение возникло. Кроме того, если вы запускаете Win32, у них есть SEH, который запускается в программах на C.

Вы можете стать глупыми и делать продолжение:

 void step_1(int a, int b, int c, void (*step_2)(int), void (*err)(void *) ) { if (!c) { err("c was 0"); } else { int r = a + b/c; step_2(r); } } 

Вероятно, это не то, что вы хотите сделать, но сколько функциональных языков программирования используется, и даже чаще, как они моделируют свой код для оптимизации.

А сейчас нечто соверешнно другое…

Другим подходом является использование структуры, содержащей ваши данные об ошибках, например:

 struct ErrorInfo { int errorCode; char *errorMessage; #if DEBUG char *functionName; int lineNumber; #endif } 

Лучший способ использовать это – вернуть результаты вашего метода в качестве кода возврата (например, «FALSE for failed», или «указатель файла или NULL, если он не работает», или «размер буфера или 0, если он не работает» и т. Д.). ) и передать в ErrorInfo в качестве параметра, который будет вызываться вызываемой функцией, если что-то не удастся.

Это дает богатую отчетность об ошибках: если метод выходит из строя, вы можете заполнить более простой код ошибки (например, сообщение об ошибке, строку кода и файл сбоя или что-то еще). Самое приятное в том, что это структура, так это то, что если вы думаете о чем-то, что-нибудь, полезное позже, вы можете просто добавить его – например, в моей структуре выше я разрешил для сборки отладки включать местоположение ошибки ( файл / строка), но вы можете добавить дамп всего стека вызовов в любой момент без изменения какого-либо кода клиента.

Вы можете использовать глобальную функцию для заполнения ErrorInfo, чтобы можно было корректно управлять возвратом ошибки, и вы можете обновить структуру, чтобы обеспечить дополнительную информацию:

 if (error) { Error(pErrorInfo, 123, "It failed"); return(FALSE); } 

… и вы можете иметь варианты этой функции, которые возвращают FALSE, 0 или NULL, чтобы позволить большинству ошибок возвращаться к одной строке:

 if (error) return(ErrorNull(pErrorInfo, 123, "It failed")); 

Это дает вам много преимуществ classа Exception на других языках (хотя вызывающему все еще нужно обрабатывать ошибки – абоненты должны проверять коды ошибок и, возможно, придется возвращаться раньше, но они ничего не могут сделать или рядом, ничего и разрешить ошибку распространять резервную копию цепочки вызовов, пока один из них не захочет ее обрабатывать, подобно исключению.

Кроме того, вы можете пойти дальше, чтобы создать цепочку отчетов об ошибках (например, «InnerException»):

 struct ErrorInfo { int errorCode; char *errorMessage; ... ErrorInfo *pInnerError; // Pointer to previous error that may have led to this one } 

Затем, если вы «поймаете» ошибку из вызываемой функции, вы можете создать новое описание ошибки более высокого уровня и вернуть цепочку этих ошибок. например: «Скорость мыши вернется к значению по умолчанию» (потому что) «Блок предпочтений« MousePrefs »не может быть расположен» (потому что) «Сбой чтения XML» (потому что) «Файл не найден».

т.е.

 FILE *OpenFile(char *filename, ErrorInfo *pErrorInfo) { FILE *fp = fopen(filename, "rb"); if (fp == NULL) return(ChainedErrorNull(pErrorInfo, "Couldn't open file")); return(fp); } XmlElement *ReadPreferenceXml(ErrorInfo *pErrorInfo) { if (OpenFile("prefs.xml", pErrorInfo) == NULL) return(ChainedErrorNull(pErrorInfo, "Couldn't read pref")); ... } char *ReadPreference(char *prefName, ErrorInfo *pErrorInfo) { XmlElement *pXml = ReadPreferenceXml(pErrorInfo); if (pXml == NULL) return(ChainedErrorNull(pErrorInfo, "Couldn't read pref")); ... } 

Что-то, что я недавно видел, это этот idom:

 int err; do { err = func1 (...); if (!err) break; err = func2 (...); if (!err) break; err = func3 (...); if (!err) break; /* add more calls here */ } while (0); if (err) { /* handle the error here */ return E_ERROR; /* or something else */ } else { return E_SUCCESS; } 

Про аргументы:

Он избегает goto (злоупотребляет комбинацией while (0) / break для этого). Зачем вам это делать? Он удерживает циклическую сложность и по-прежнему будет проходить большинство статических проверок анализатора кода (MISRA кто-нибудь?). Для проектов, которые проходят тестирование против циклической сложности, этот бог отправлен, потому что он сохраняет все элементы инициализации вместе.

Аргументы аргумента:

Значение конструкции do / while цикла не очевидно, потому что конструкция цикла используется как дешевая замена goto, и это можно увидеть только в хвосте цикла. Я уверен, что в первый раз эта конструкция вызовет множество «WTF» -моментов.

По крайней мере, комментарий необходим, чтобы объяснить, почему код написан так, как он требуется.

Вот довольно информативная статья и тестовый файл в серии статей IBM Unix:

Ошибки: errno в программах UNIX

Работа со стандартным механизмом ошибок

https://www.ibm.com/developerworks/aix/library/au-errnovariable/

Другим хорошим примером реализации кодов выхода является исходный код curl (man 1 curl).

Если вы работаете с определенным контекстом, я думаю, что следующий шаблон очень приятный. Основная идея заключается в том, что операции с условием установки ошибок не являются операциями, поэтому проверка ошибок может быть отложена до того момента, когда это будет удобно!

Конкретный пример: контекст десериализации. Декодирование любого элемента может завершиться неудачей, но функция может продолжаться без проверки ошибок, поскольку все функции decode_* не являются операциями, когда запись сериализации находится в состоянии ошибки. Это вопрос удобства или возможности или оптимизации для вставки decode_has_error . В приведенном ниже примере проверка ошибок отсутствует, вызывающий абонент позаботится об этом.

 void list_decode(struct serialization_record *rec, struct list *list, void *(*child_decode)(struct serialization_record *)) { uint32_t length; decode_begin(rec, TAG); decode_uint32(rec, &length); for (uint32_t i = 0; i < length; i++) { list_append(list, child_decode(rec)); } decode_end(rec, TAG); }