Обработка ошибок в коде 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 ]

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

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

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

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

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

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

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

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

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

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

19 голосов
/ 09 декабря 2016

Есть хороший набор слайдов из CMERT CERT с рекомендациями относительно того, когда использовать каждый из распространенных методов обработки ошибок C (и C ++). Одно из лучших слайдов - это дерево решений:

Error Handling Decision Tree

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

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

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

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

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

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

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


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

16 голосов
/ 23 декабря 2008

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

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

    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 также позволяет отображать имя перечисления в отладчике. Это упрощает отладку без необходимости постоянно обращаться к заголовочному файлу. Также полезно иметь функцию для перевода этого перечисления в строку.

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

9 голосов
/ 19 апреля 2011

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

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

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

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

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

7 голосов
/ 08 августа 2011

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

6 голосов
/ 21 июля 2016

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

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

У него есть некоторые преимущества перед подходом с возвращаемым значением:

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

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

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

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

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

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

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

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

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

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

Что бы это ни стоило, Какао также применяет подобный подход. Несколько методов возвращают BOOL и принимают параметр NSError **, чтобы при ошибке они устанавливали ошибку и возвращали NO. Тогда обработка ошибок выглядит так:

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

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

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