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

152

Что вы считаете «наилучшей практикой», когда речь идет об обработке ошибок согласованным способом в 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
}

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

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

РЕДАКТИРОВАТЬ: C ++ конкретные идеи по этому вопросу также было бы интересно услышать, если они не связаны с исключениями, так как на данный момент это не вариант для меня ...

Laserallan
источник
Я изучал C только около двух недель, но у меня появилось ощущение, что OUT-параметры являются возвращаемым значением defacto для большинства функций, так как он позволяет избежать накладных расходов по возвращению структур по значению и уменьшает необходимость освобождения памяти, так как большинство переменных находятся в стеке. Так как я не использую «return» для фактического значения функции, я могу использовать его для обработки ошибок большую часть времени.
Джоэл Робертс

Ответы:

74

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

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

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

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

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

Нильс Пипенбринк
источник
5
Почему вы говорите: «Эта идея делает многопоточное использование немного сложным». Какая часть усложняется многопоточностью? Можете ли вы привести быстрый пример?
SayeedHussain
1
@crypticcoder Проще говоря: глобальный ответ об ошибке может быть вызван в любом контексте потока. Если вы просто распечатаете ошибку, у вас не возникнет никаких проблем. Если вы попытаетесь исправить проблемы, вам придется выяснить, какой вызывающий поток вызвал ошибку, и это усложняет ситуацию.
Нильс Пипенбринк
9
Что делать, если вы хотите сообщить более подробную информацию об ошибке? Например, у вас есть ошибка синтаксического анализатора, и вы хотите указать номер строки и столбец синтаксической ошибки, а также способ красиво их распечатать.
Panzi
1
@panzi, тогда вам, очевидно, нужно вернуть структуру (или использовать указатель out, если структура действительно велика) и иметь функцию для форматирования структуры в виде строки.
Вингер Сендон,
Я продемонстрирую ваши первые 2 патрона в коде здесь: stackoverflow.com/questions/385975/error-handling-in-c-code/…
Габриэль Стейплс
92

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

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

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

AShelly
источник
1
Есть пример утверждений в C? (Я очень зеленый для C)
Thomthom
Обычно это так же просто, как assert(X)где X - любое допустимое утверждение C, которое вы хотите быть истинным. см. stackoverflow.com/q/1571340/10396 .
AShelly
14
Тьфу, абсолютно никогда не используйте утверждения в коде библиотеки ! Кроме того, не смешивайте различные стили обработки ошибок в одном куске кода, как это делали другие ...
mirabilos
10
Я, конечно, согласен с тем, чтобы не смешивать стили. Мне любопытно, что вы обосновываете утверждения. Если в документации по моей функции написано «аргумент X не должен быть НЕДЕЙСТВИТЕЛЕН» или «Y должен быть членом этого перечисления», то что не так с assert(X!=NULL);или assert(Y<enumtype_MAX);? Посмотрите этот ответ для программистов и вопрос, на который он ссылается, для более подробной информации о том, почему я считаю, что это правильный путь.
AShelly
8
@AShelly Проблема с утверждениями о том, что их обычно нет в сборках релизов.
Кальмарий
29

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

Дерево решений по обработке ошибок

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

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

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

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

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

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

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


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

Praxeolitic
источник
5
Вы можете заметить, что в стандартах Google C ++ по-прежнему говорится, что мы не используем исключения C ++.
Джонатан Леффлер
19

Я использую первый подход всякий раз, когда я создаю библиотеку. Есть несколько преимуществ использования перечисления 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

Используйте 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;
}
Амир Саниян
источник
2
Второй блок кода основан на более ранней версии кода на странице Франческо Нидито, на которую есть ссылка в верхней части ответа. ETRYКод был пересмотрен , так как этот ответ был написан.
Джонатан Леффлер
2
Setjmp - ужасная стратегия обработки ошибок. Это дорого, подвержено ошибкам (с изменчивыми локальными переменными, не сохраняющими свои измененные значения и все) и теряет ресурсы, если вы выделяете их между вызовами setjmp и longjmp. Вы должны быть в состоянии сделать как 30 возвратов и проверок int-val, прежде чем окупить стоимость sigjmp / longjmp. Большинство стеков вызовов не заходят так глубоко, особенно если вы не усердно работаете с рекурсией (и если вы это делаете, у вас возникают проблемы с производством, кроме стоимости возврата + проверки).
PSkocik
1
Если вы используете malloc'у память, а затем выбросите ее, она просто будет течь вечно. Кроме того, setjmpэто дорого, даже если не выдается никакой ошибки, это потребует немало времени процессора и места в стеке. При использовании gcc для Windows вы можете выбирать между различными методами обработки исключений для C ++, один из которых основан на setjmpи делает ваш код на 30% медленнее на практике.
Меки
7

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

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

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

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

Альнитак
источник
1
Для справки, одна библиотека, которую я видел, использует последний подход - API программирования Maya. Это библиотека C ++, а не C. Это довольно противоречиво в том, как он обрабатывает свои ошибки, и иногда ошибка передается как возвращаемое значение, а в других случаях он передает результат в качестве ссылки.
Laserallan
1
не забывайте strtod, последний аргумент не только указывает на ошибки, но и делает это тоже.
Quinmars
7

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

Генри
источник
7

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

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

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

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

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

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

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

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

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

Calmarius
источник
6

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

  • Повторяющиеся номера ошибок, это можно решить с помощью глобального файла errors.h.
  • Забыв проверить код ошибки, это должно быть решено с помощью cluebat и долгими часами отладки. Но в конце вы узнаете (или вы будете знать, что кто-то другой будет делать отладку).
Тун Крижте
источник
2
Вторая проблема может быть решена с помощью надлежащего уровня предупреждения компилятора, надлежащего механизма просмотра кода и инструментов статического анализатора кода.
Илья
1
Вы также можете работать по принципу: если вызывается функция API, а возвращаемое значение не проверяется, возникает ошибка.
Джонатан Леффлер
6

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

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

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

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


источник
5

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

naasking
источник
Спасибо, что дал мне знать. Было интересно посмотреть.
Laserallan
5

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

Это предполагает, что переменная типа дескриптора является экземпляром, в котором работают все функции 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;
}
SirDarius
источник
4

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

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

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

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функция не может быть вызвана для правильной обработки ошибок.

Илья
источник
3

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

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

Антуан С.
источник
3

Вот простая программа, чтобы продемонстрировать первые 2 пули ответа Нильса Пипенбринка здесь .

Его первые 2 пули:

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

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

Предположим, вы написали модуль с именем mymodule. Во-первых, в mymodule.h вы определяете свои коды ошибок на основе enum и пишете несколько строк ошибок, соответствующих этим кодам. Здесь я использую массив C strings ( char *), который хорошо работает только в том случае, если ваш первый код ошибки на основе enum имеет значение 0, и вы не манипулируете числами после этого.Если вы используете номера кодов ошибок с пробелами или другими начальными значениями, вам просто придется перейти от использования сопоставленного массива C-строк (как я делаю ниже) к использованию функции, которая использует оператор switch или операторы if / else if отобразить коды ошибок enum в печатные строки C (которые я не демонстрирую). Выбор за вами.

mymodule.h

/// @brief Error codes for library "mymodule"
typedef enum mymodule_error_e
{
    /// No error
    MYMODULE_ERROR_OK = 0,
    
    /// Invalid arguments (ex: NULL pointer where a valid pointer is required)
    MYMODULE_ERROR_INVARG,

    /// Out of memory (RAM)
    MYMODULE_ERROR_NOMEM,

    /// Make up your error codes as you see fit
    MYMODULE_ERROR_MYERROR, 

    // etc etc
    
    /// Total # of errors in this list (NOT AN ACTUAL ERROR CODE);
    /// NOTE: that for this to work, it assumes your first error code is value 0 and you let it naturally 
    /// increment from there, as is done above, without explicitly altering any error values above
    MYMODULE_ERROR_COUNT,
} mymodule_error_t;

// Array of strings to map enum error types to printable strings
// - see important NOTE above!
const char* const MYMODULE_ERROR_STRS[] = 
{
    "MYMODULE_ERROR_OK",
    "MYMODULE_ERROR_INVARG",
    "MYMODULE_ERROR_NOMEM",
    "MYMODULE_ERROR_MYERROR",
};

// To get a printable error string
const char* mymodule_error_str(mymodule_error_t err);

// Other functions in mymodule
mymodule_error_t mymodule_func1(void);
mymodule_error_t mymodule_func2(void);
mymodule_error_t mymodule_func3(void);

mymodule.c содержит мою функцию отображения для сопоставления кодов ошибок enum с печатными строками C:

mymodule.c

#include <stdio.h>

/// @brief      Function to get a printable string from an enum error type
/// @param[in]  err     a valid error code for this module
/// @return     A printable C string corresponding to the error code input above, or NULL if an invalid error code
///             was passed in
const char* mymodule_error_str(mymodule_error_t err)
{
    const char* err_str = NULL;

    // Ensure error codes are within the valid array index range
    if (err >= MYMODULE_ERROR_COUNT)
    {
        goto done;
    }

    err_str = MYMODULE_ERROR_STRS[err];

done:
    return err_str;
}

// Let's just make some empty dummy functions to return some errors; fill these in as appropriate for your 
// library module

mymodule_error_t mymodule_func1(void)
{
    return MYMODULE_ERROR_OK;
}

mymodule_error_t mymodule_func2(void)
{
    return MYMODULE_ERROR_INVARG;
}

mymodule_error_t mymodule_func3(void)
{
    return MYMODULE_ERROR_MYERROR;
}

main.c содержит тестовую программу для демонстрации вызова некоторых функций и вывода из них некоторых кодов ошибок:

main.c

#include <stdio.h>

int main()
{
    printf("Demonstration of enum-based error codes in C (or C++)\n");

    printf("err code from mymodule_func1() = %s\n", mymodule_error_str(mymodule_func1()));
    printf("err code from mymodule_func2() = %s\n", mymodule_error_str(mymodule_func2()));
    printf("err code from mymodule_func3() = %s\n", mymodule_error_str(mymodule_func3()));

    return 0;
}

Вывод:

Демонстрация кодов ошибок на основе перечисления в коде ошибки C (или C ++)
из mymodule_func1 () = MYMODULE_ERROR_OK
код ошибки из mymodule_func2 () = MYMODULE_ERROR_INVARG
код ошибки из mymodule_func3 () = MYMODULE_ERROR_

Ссылки:

Вы можете запустить этот код самостоятельно здесь: https://onlinegdb.com/ByEbKLupS .

Габриэль Стейплс
источник
2

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

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

if (MyFunc())
 DoSomething();

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

SmacL
источник
2

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

Как

Есть 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]);
}

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

Перезвони

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

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

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

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

Аллан Уинд
источник
1

РЕДАКТИРОВАТЬ: Если вам нужен доступ только к последней ошибке, и вы не работаете в многопоточной среде.

Вы можете возвратить только 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
}
Игорь Окс
источник
На самом деле, но это не C, это может быть предоставлено операционной системой или нет. Если вы работаете, например, с операционными системами реального времени, то у вас их нет.
Илья
1

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

dmityugov
источник
1

Я предпочитаю обработку ошибок в 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
Хорошая техника. Я нахожу даже аккуратнее с goto's вместо повторного if' s. Рекомендации: раз , два .
Ant_222
0

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

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

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

Ant_222
источник