Обработка ошибок в коде C

Что вы считаете "лучшей практикой", когда речь идет об ошибках обработки ошибок последовательным образом в библиотеке 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: Особые идеи С++ по этому поводу также будут интересны, если они не будут включать исключения, поскольку это не вариант для меня в данный момент...

Ответ 1

Мне нравится ошибка как способ возврата. Если вы разрабатываете api и хотите использовать свою библиотеку как можно более безболезненно, подумайте об этих дополнениях:

  • хранить все возможные состояния ошибок в одном перечислении typedef'ed и использовать его в своей библиотеке. Не просто возвращайте ints или, что еще хуже, смешивайте ints или разные перечисления с помощью возвращаемых кодов.

  • предоставляет функцию, которая преобразует ошибки во что-то читаемое человеком. Может быть просто. Просто error-enum in, const char * out.

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

Надеюсь, это поможет.

Ответ 2

Я использовал оба подхода, и оба они отлично работали для меня. Какой бы я ни использовал, я всегда стараюсь применить этот принцип:

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

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

Ответ 3

Я использую первый подход, когда создаю библиотеку. Существует несколько преимуществ использования typedef'ed enum в качестве кода возврата.

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

    rc = func(..., int **return_array, size_t *array_length);
    
  • Он позволяет упростить стандартизованную обработку ошибок.

    if ((rc = func(...)) != API_SUCCESS) {
       /* Error Handling */
    }
    
  • Он позволяет выполнять простые обработки ошибок в библиотечной функции.

    /* Check for valid arguments */
    if (NULL == return_array || NULL == array_length)
        return API_INVALID_ARGS;
    
  • Использование перечисления typedef'ed также позволяет отображать имя перечисления в отладчике. Это позволяет упростить отладку без необходимости постоянно обращаться к файлу заголовка. Также полезно использовать функцию для преобразования этого перечисления в строку.

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

Ответ 4

Там есть хороший набор слайдов из CMU CERT с рекомендациями по использованию каждой из общих методов обработки ошибок C (и C++). Одним из лучших слайдов является это дерево решений:

Error Handling Decision Tree

Я лично изменил бы две вещи об этой блок-карте.

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

Во-вторых, не всегда целесообразно использовать исключения в C++. Исключения хороши, потому что они могут уменьшить количество исходных кодов, предназначенных для обработки ошибок, они в основном не влияют на сигнатуры функций, и они очень гибки в том, какие данные они могут передать в callstack. С другой стороны, исключения могут быть неправильным выбором по нескольким причинам:

  1. C++ исключения имеют очень специфическую семантику. Если вы не хотите эту семантику, то исключения C++ являются плохим выбором. Исключение должно быть рассмотрено сразу же после того, как оно будет выброшено, и дизайн будет способствовать тому, что ошибка должна будет отключить callstack на нескольких уровнях.

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

  3. C++ исключения относительно дороги, но этот недостаток в основном раздувается для программ, которые используют разумное использование исключений. Программа просто не должна бросать исключения на кодировку, в которой производительность является проблемой. Неважно, насколько быстро ваша программа может сообщить об ошибке и выйти.

  4. Иногда C++ исключения недоступны. Либо они буквально недоступны в одной реализации C++, либо одни правила кода запрещают их.


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

Ответ 5

Используйте 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;
}

Ответ 6

Я лично предпочитаю прежний подход (возврат индикатора ошибки).

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

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

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

Ответ 7

В прошлом я много программировал на C. И я действительно оценил возвращаемое значение кода ошибки. Но есть несколько возможных ловушек:

  • Повторяющиеся номера ошибок, это можно решить с помощью глобального файла errors.h.
  • Забыв проверить код ошибки, это должно быть разрешено с помощью cluebat и длинных отладочных часов. Но в конце вы узнаете (или вы узнаете, что кто-то еще сделает отладку).

Ответ 8

Подход UNIX наиболее похож на ваше второе предложение. Верните либо результат, либо значение "пошло не так". Например, open вернет дескриптор файла при успешном выполнении или -1 при ошибке. При сбое он также устанавливает errno, внешнее глобальное целое, чтобы указать, какой сбой произошел.

Для того, что стоит, Cocoa также применяет подобный подход. Ряд методов возвращает BOOL и принимает параметр NSError **, поэтому при ошибке они устанавливают ошибку и возвращают NO. Затем обработка ошибок выглядит так:

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

который находится где-то между двумя вашими опциями: -).

Ответ 9

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

Ответ 10

Возвращаемый код ошибки - обычный подход для обработки ошибок в C.

Но в последнее время мы также экспериментировали с исходящим методом указателей ошибок.

Он имеет некоторые преимущества по сравнению с методом возвращаемого значения:

  • Вы можете использовать возвращаемое значение для более значимых целей.

  • Чтобы записать этот параметр ошибки, вам нужно обработать ошибку или распространить ее. (Вы никогда не забываете проверить возвращаемое значение fclose, не так ли?)

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

  • Установив точку останова данных в переменной ошибки, вы можете поймать, где именно произошла ошибка. Установив условную точку останова, вы также можете поймать определенные ошибки.

  • Это упрощает автоматизацию проверки, обрабатываете ли вы все ошибки. Соглашение о коде может заставить вас вызвать указатель на ошибку как err, и это должен быть последний аргумент. Таким образом, script может соответствовать строке err);, а затем проверить, следует ли ей if (*err. Фактически на практике мы сделали макрос с именем CER (check err return) и CEG (check err goto). Поэтому вам не нужно вводить его всегда, когда мы просто хотим вернуться к ошибке и можем уменьшить визуальный беспорядок.

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

Ответ 11

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

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

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

Ответ 12

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

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

Ответ 14

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

Как

Существует 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):

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

Из группы

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

Какие

  1. Индикатор
  2. Код
  3. объект
  4. Перезвони

Индикатор

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

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

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

Код

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

объект

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

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

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

Перезвони

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

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

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

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

Ответ 15

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

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 не может обрабатывать обработку ошибок.

Ответ 16

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

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

if (MyFunc())
 DoSomething();

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

Ответ 17

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

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 в обоих представленных вами методах).

Ответ 18

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

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

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
}

Ответ 19

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

Ответ 20

Я предпочитаю обработку ошибок в 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

Ответ 21

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

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

Когда у вас много ошибок, это небольшое упрощение действительно помогает.