Подтвердить что ты не робот

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

Возвращаясь к работе с 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 в наши дни.

4b9b3361

Ответ 1

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

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();
}

Ответ 2

Если у вас есть ресурсы, которые должны быть выпущены в конце, тогда иногда может быть полезно старое надежное 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;
}

Ответ 3

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

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

err = func2(...);
if (err) {
    return err;
}

err = func3(...);

return err;

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

ИЗМЕНИТЬ

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

Ответ 4

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

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

Ответ 5

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

/* 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");
}

Ответ 6

Другие предложили хорошие идеи. Вот идиомы, которые я видел.
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 как глобальные.

В качестве альтернативы вы можете использовать макросы для простого прохождения аргумента. Я бы предложил способ реализации 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;
}

-

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

Ответ 7

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

Ответ 8

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

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);
     }
}

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

Ответ 9

И теперь для чего-то совершенно другого...

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

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

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

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

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

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

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

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

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

Кроме того, вы можете пойти дальше, чтобы создать цепочку сообщений об ошибках (например, "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"));
    ...
}

Ответ 10

Что-то, что я недавно видел, это 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" -моментов.

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

Ответ 11

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

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

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

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

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

Ответ 12

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

Конкретный пример: контекст десериализации. Декодирование любого элемента может завершиться неудачей, но функция может продолжаться без проверки ошибок, поскольку все функции 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);
}