Обработка ошибок в коде C - PullRequest
137 голосов
/ 22 декабря 2008

Что вы считаете «наилучшей практикой», когда речь идет об обработке ошибок согласованным способом в библиотеке C.

Есть два способа, о которых я думал:

Всегда возвращайте код ошибки. Типичная функция будет выглядеть так:

MYAPI_ERROR getObjectSize(MYAPIHandle h, int* returnedSize);

Всегда используйте подход указателя ошибки:

int getObjectSize(MYAPIHandle h, MYAPI_ERROR* returnedError);

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

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

Что выглядит лучше, чем код обработки ошибок здесь.

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

Однако я думаю, что использование возвращаемого значения для возврата данных делает код более читабельным. Очевидно, что что-то было записано в переменную размера во втором примере.

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

EDIT: Особые идеи C ++ по этому вопросу также было бы интересно услышать, если они не связаны с исключениями, поскольку в данный момент это не вариант для меня ...

Ответы [ 21 ]

5 голосов
/ 28 января 2013

Вот подход, который, на мой взгляд, интересен, но требует некоторой дисциплины.

Предполагается, что переменная типа дескриптора является экземпляром, в котором работают все функции API.

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

Пример:

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;
}
4 голосов
/ 22 декабря 2008

Первый подход лучше ИМХО:

  • Так проще написать функцию. Когда вы замечаете ошибку в середине функции, вы просто возвращаете значение ошибки. При втором подходе вам нужно присвоить значение ошибки одному из параметров, а затем вернуть что-то .... но что бы вы вернули - у вас нет правильного значения и вы не возвращаете значение ошибки.
  • он более популярен, поэтому его будет легче понять, поддерживать
4 голосов
/ 24 сентября 2010

Я тоже недавно обдумывал эту проблему и написал некоторые макросы для C, которые имитируют семантику try-catch-finally с использованием чисто локальных возвращаемых значений. Надеюсь, вы найдете это полезным.

3 голосов
/ 20 мая 2018

Я несколько раз сталкивался с вопросами и ответами и хотел дать более полный ответ. Я думаю, что лучший способ думать об этом - как возвратить ошибки вызывающей стороне, и что вы вернете.

Как

Есть 3 способа вернуть информацию из функции:

  1. Возвращаемое значение
  2. Аргумент (ы)
  3. Out of Band, который включает нелокальное goto (setjmp / longjmp), файловые или глобальные переменные области действия, файловая система и т. д.

Возвращаемое значение

Можно вернуть только значение одного объекта, однако это может быть произвольный комплекс. Вот пример функции возврата ошибки:

  enum error hold_my_beer();

Одним из преимуществ возвращаемых значений является то, что они позволяют объединять вызовы для менее навязчивой обработки ошибок:

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

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

Аргумент (ы)

Вы можете возвращать больше, используя более одного объекта через аргументы, но передовая практика предлагает сохранить общее количество аргументов низким (скажем, <= 4): </p>

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

Out of Band

С помощью setjmp () вы определяете место и то, как вы хотите обработать значение int, и вы передаете управление этому месту с помощью longjmp (). См. Практическое использование setjmp и longjmp в C .

Что

  1. Индикатор
  2. Код
  3. Object
  4. Обратный вызов

Индикатор

Индикатор ошибки говорит только о наличии проблемы, но ничего не говорит о природе указанной проблемы:

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

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

Код

Код ошибки сообщает вызывающей стороне о природе проблемы и может позволить подходящий ответ (из вышеизложенного). Это может быть возвращаемое значение или как пример look_ma () над аргументом ошибки.

Object

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

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

Вместо того, чтобы предварительно выделять массив ошибок, вы также можете (пере) распределять его динамически по мере необходимости.

Обратный вызов

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

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

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

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

3 голосов
/ 22 декабря 2008

Я определенно предпочитаю первое решение:

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

Я бы немного изменил его, чтобы:

int size;
MYAPIError rc;

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

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

И если мы уже говорим об обработке ошибок, я бы предложил goto Error; в качестве кода обработки ошибок, если только некоторая функция undo не может быть вызвана для правильной обработки ошибок.

2 голосов
/ 09 июля 2017

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

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

Затем в вызываемой функции:

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

Обратите внимание, что при использовании следующего метода оболочка будет иметь размер MyType плюс один байт (на большинстве компиляторов), что весьма выгодно; и вам не придется помещать другой аргумент в стек при вызове функции (returnedSize или returnedError в обоих представленных вами методах).

2 голосов
/ 22 декабря 2008

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

Я лично возвращаю коды ошибок как отрицательные целые числа с no_error как ноль, но это оставляет вас с возможной следующей ошибкой

if (MyFunc())
 DoSomething();

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

1 голос
/ 19 ноября 2011

Я предпочитаю обработку ошибок в C, используя следующую технику:

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

Источник: http://blog.staila.com/?p=114

1 голос
/ 22 декабря 2008

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

1 голос
/ 22 декабря 2008

EDIT: если вам нужен доступ только к последней ошибке, и вы не работаете в многопоточной среде.

Вы можете возвращать только true / false (или какой-то #define, если вы работаете в C и не поддерживает переменные bool), и имеете глобальный буфер Error, который будет содержать последнюю ошибку:

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
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...