Помилка обробки коду С


152

Що ви вважаєте "найкращою практикою", коли мова йде про помилки, що відповідають помилкам у бібліотеці С.

Я думав про два способи:

Завжди повертайте код помилки. Типова функція виглядала б так:

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 ++ конкретні ідеї щодо цього також було б цікаво почути до тих пір, поки вони не передбачають винятків, оскільки це наразі для мене не варіант ...


Я вивчав С лише близько двох тижнів, але відчуваю, що параметри OUT - це значення, що повертається до дефакто, для більшості функцій, оскільки це дозволяє уникнути накладних витрат повертаються структур за вартістю і зменшує необхідність розміщення пам'яті, оскільки більшість змінних є в стеці. Оскільки я не використовую "return" для фактичного значення функції, я вільний використовувати його для обробки помилок більшу частину часу.
Джоель Робертс

Відповіді:


74

Мені подобається помилка як спосіб повернення-значення. Якщо ви розробляєте api і хочете максимально безболісно використовувати свою бібліотеку, подумайте про ці доповнення:

  • зберігайте всі можливі стани помилок в одному типіdedef'ed enum і використовуйте його у своїй lib. Не просто повертайте вставки або ще гірше, змішуйте вставки або різні перерахування із зворотними кодами.

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

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

Сподіваюся, це допомагає.


5
Чому ви кажете: "Ця ідея робить багатопотокове використання трохи складним". Яка частина утруднюється багаторядкою? Чи можете ви навести короткий приклад?
SayeedHussain

1
@crypticcoder Просто сказано: глобальний зворотний виклик помилки може викликатися в будь-якому контексті потоку. Якщо ви просто роздрукуєте помилку, у вас не виникне жодних проблем. Якщо ви спробуєте виправити проблеми, вам доведеться з’ясувати, який потік викликав помилку, і це ускладнює ситуацію.
Нільс Піпенбрінк

9
Що робити, якщо ви хочете повідомити більше деталей помилки? Наприклад, у вас є помилка аналізатора і ви хочете вказати номер рядка та стовпця синтаксичної помилки та спосіб їх гарного друку.
panzi

1
@panzi, тоді вам, очевидно, потрібно повернути структуру (або використовувати вказівник, якщо структура дійсно велика) і мати функцію для форматування структури у вигляді рядка.
Вінгер Сендон

Я демонструю ваші перші 2 кулі в коді тут: stackoverflow.com/questions/385975/error-handling-in-c-code/…
Габріель

92

Я використовував обидва підходи, і вони обидва працювали добре для мене. Який би я не використовував, я завжди намагаюся застосовувати цей принцип:

Якщо єдиними можливими помилками є помилки програміста, не повертайте код помилки, використовуйте твердження всередині функції.

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


1
Зробив приклад тверджень у С? (Я дуже зелений до C)
thomthom

Зазвичай це так просто, як assert(X)де X - це будь-яке дійсне твердження C, яке ви хочете бути правдивим. див. stackoverflow.com/q/1571340/10396 .
AShelly

14
Тьфу, абсолютно ніколи не використовуйте твердження в бібліотечному коді ! Крім того, не змішуйте різні стилі керування помилками в одному фрагменті коду, як це робили інші…
mirabilos

10
Я, безумовно, згоден з приводу того, що не змішувати стилі. Мені цікаво ваші міркування щодо тверджень. Якщо в моїй документації про функцію сказано, що "аргумент X не повинен бути NULL" або "Y повинен бути членом цієї перерахунку", ніж те, що не так assert(X!=NULL);або assert(Y<enumtype_MAX);? Дивіться цю відповідь про програмістів і питання, на яке вона посилається, для більш детальної інформації про те, чому я вважаю, що це правильний шлях.
AShelly

8
@AShelly Проблема з твердженням, що вони зазвичай не існують у версії версій.
Кальмарій

29

Є гарний набір слайдів із CERT CMU з рекомендаціями щодо використання кожного із поширених C (та C ++) методів обробки помилок. Одне з найкращих слайдів - це дерево рішень:

Помилка обробки дерева рішень

Я особисто змінив би дві речі щодо цієї схеми.

По-перше, я б уточнив, що іноді об'єкти повинні використовувати зворотні значення для позначення помилок. Якщо функція витягує дані лише з об'єкта, але не мутує об'єкт, то цілісність самого об'єкта не загрожує, а вказівка ​​помилок із використанням повернутого значення є більш доцільною.

По-друге, не завжди доцільно використовувати винятки в C ++. Винятки хороші тим, що вони можуть зменшити кількість вихідного коду, присвяченого обробці помилок, вони здебільшого не впливають на підписи функцій, і вони дуже гнучкі в тому, які дані вони можуть передавати до ставки виклику. З іншого боку, винятки можуть бути не правильним вибором з кількох причин:

  1. Винятки C ++ мають дуже особливу семантику. Якщо ви не хочете цієї семантики, то винятки C ++ - це поганий вибір. Виняток повинен бути вирішений відразу після викидання, і дизайн сприяє випадку, коли помилка потребує розкручування стола на кілька рівнів.

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

  3. Винятки на C ++ є відносно дорогими, але цей недолік здебільшого перекритий для програм, які розумно використовують винятки. Програма просто не повинна викидати винятки на кодовий шлях, коли продуктивність викликає занепокоєння. Не дуже важливо, наскільки швидко ваша програма може повідомити про помилку та вийти.

  4. Іноді винятки C ++ недоступні. Або вони буквально недоступні в C ++ реалізації, або вказівки щодо коду забороняють їх.


Оскільки вихідний питання було про багатопотоковому контексті, я думаю , що метод локального індикатора помилки (що описано в SirDarius «s відповіді ) був недооціненим в оригінальних відповідях. Це безпечно в потоці, не змушує помилку негайно усунути абонент, і може поєднати довільні дані, що описують помилку. Мінус полягає в тому, що він повинен утримуватися об'єктом (або, мабуть, якимось чином пов'язаний зовні), і, можливо, простіше ігнорувати, ніж код повернення.


5
Ви можете зауважити, що стандарти кодування C ++ Google все ще говорять, що ми не використовуємо винятки C ++.
Джонатан Леффлер

19

Я використовую перший підхід, коли створюю бібліотеку. Є кілька переваг використання endef'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;
  • Використання endef'ed enum також дозволяє назві enum бути видно у налагоджувачі. Це дозволяє простіше налагоджувати без необхідності постійно звертатися до файлу заголовка. Корисна також функція для перекладу цього перерахунку в рядок.

Найголовніше питання незалежно від використовуваного підходу - це бути послідовним. Це стосується іменування функцій та аргументів, упорядкування аргументів та обробки помилок.


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 - жахлива стратегія поводження з помилками. Це дорого, схильне до помилок (w / енергонезалежних змінених місцевих жителів, не зберігаючи змінених значень і всіх) і витоку ресурсів, якщо ви виділите будь-які між викликами setjmp і longjmp. Ви повинні мати змогу зробити як-небудь 30 повернень та int-val перевірок, перш ніж повернути вартість sigjmp / longjmp. Більшість ставок дзвінків не надто глибокі, особливо якщо ви не сильно ставитеся до рекурсії (а якщо у вас є проблеми з перф, крім вартості повернення + чеків).
PSkocik

1
Якщо ви занепокоїли пам'ять, а потім кинете, пам'ять просто просочиться назавжди. Крім того, setjmpце дорого, навіть якщо жодна помилка не буде викинута, це забирає зовсім небагато часу в процесорі та місця у стеці. Використовуючи gcc для Windows, ви можете вибирати різні методи обробки винятків для C ++, один з яких заснований, setjmpі це робить ваш код на 30% повільніше на практиці.
Мецькі

7

Я особисто віддаю перевагу колишньому підходу (повертаючи показник помилки).

У необхідних випадках результат повернення повинен просто вказувати на те, що сталася помилка, і інша функція використовується для з'ясування точної помилки.

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

Я не можу придумати будь-яку бібліотеку, яку я використав, що стосується останнього підходу з об'єктом помилки, переданим у якості вказівника. stdioі т. д. всі йдуть із зворотним значенням.


1
Для запису одна бібліотека, яку я бачив, використовує останній підхід, - програмування API Maya. Це бібліотека c ++, а не C. Це досить непослідовно в тому, як він обробляє свої помилки, а іноді помилка передається як повернене значення, а в інший раз вона передає результат як еталон.
Ласераллан

1
не забувайте strtod, добре, останній аргумент не тільки для вказівки на помилки, але і це робить.
квінмар

7

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


7

Повернення коду помилки - це звичайний підхід для обробки помилок у C.

Але нещодавно ми експериментували і з підходящим підходом на покажчик помилок.

Він має деякі переваги перед підходом до повернення:

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

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

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

  • Встановивши точку розриву даних на змінній помилки, ви можете зрозуміти, звідки вперше сталася помилка. Встановивши умовну точку розриву, ви також можете вловлювати конкретні помилки.

  • Це полегшує автоматизацію перевірки, чи ви обробляєте всі помилки. Конвенція про код може змусити вас викликати покажчик помилки як errі він повинен бути останнім аргументом. Тож сценарій може відповідати рядку, err);а потім перевірити, чи за ним слід if (*err. Насправді на практиці ми зробили макрос під назвою CER(check err return) та CEG(check err goto). Таким чином, вам не потрібно вводити це завжди, коли ми просто хочемо повернути помилку, і може зменшити зорове скупчення.

Не всі функції в нашому коді мають цей вихідний параметр. Цей вихідний параметр використовується для випадків, коли ви зазвичай кидаєте виняток.


6

У минулому я багато робив програмування на С. І я дуже оцінив значення повернення коду помилки. Але є кілька можливих підводних каменів:

  • Дублювання номерів помилок, це можна вирішити за допомогою глобального файлу error.h.
  • Забувши перевірити код помилки, це повинно бути вирішено за допомогою підказки та довгих годин налагодження. Але врешті-решт ти дізнаєшся (або ти будеш знати, що налагодження буде робити хтось інший).

2
Друга проблема може бути вирішена належним рівнем попередження компілятора, належним механізмом перегляду коду та засобами аналізатора статичного коду.
Ілля

1
Ви також можете працювати за принципом: якщо функція API викликається, а повернене значення не перевірено, виникає помилка.
Джонатан Леффлер

6

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

Наскільки це варте, какао також застосовує подібний підхід. Ряд методів повертають BOOL і приймають NSError **параметр, так що при відмові вони встановлюють помилку і повертають NO. Тоді обробка помилок виглядає так:

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

що знаходиться десь між вашими двома варіантами :-).


5

Нещодавно я також розмірковував над цим питанням і написав кілька макросів для C, що імітують семантику пробного підключення, використовуючи суто локальні значення повернення. Сподіваюся, вам здається корисним.


Дякую що дали мені знати. Це було цікаво подивитися.
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;
}

4

Перший підхід краще ІМХО:

  • Простіше писати функцію таким чином. Коли ви помітите помилку в середині функції, ви просто повернете значення помилки. При другому підході вам потрібно призначити значення помилки одному з параметрів, а потім повернути щось .... але що б ви повернули - у вас немає правильного значення і ви не повертаєте значення помилки.
  • він популярніший, тому його буде простіше зрозуміти, підтримувати

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 кулі:

  • зберігайте всі можливі стани помилок в одному типіdedef'ed enum і використовуйте його у своїй lib. Не просто повертайте вставки або ще гірше, змішуйте вставки або різні перерахування із зворотними кодами.

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

Припустимо, ви написали модуль з ім’ям mymodule. По-перше, у mymodule.h ви визначаєте коди помилок на основі перерахунків, і ви пишете кілька рядків помилок, які відповідають цим кодам. Тут я використовую масив рядків C ( char *), який працює добре лише у тому випадку, якщо ваш перший код помилки на основі перерахунків має значення 0, і ви не керуєте цим числом далі. Якщо ви використовуєте кодові помилки з пробілами або іншими початковими значеннями, вам просто доведеться перейти від використання відображеного масиву C-рядків (як я це робив нижче) на використання функції, яка використовує оператор перемикання або if / else, якщо заяви зіставити з кодів помилок перерахунків на друковані рядки 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 містить мою функцію зіставлення для зіставлення кодів помилок перерахунків до друкованих рядків 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 () = MYMODMLE_ERR

Список літератури:

Ви можете самостійно запустити цей код тут: https://onlinegdb.com/ByEbKLupS .


2

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

Я особисто повертаю коди помилок як негативні цілі числа з no_error як нуль, але це залишає вас з можливою наступною помилкою

if (MyFunc())
 DoSomething();

Альтернативою є помилка, яка завжди повертається як нуль, і використовувати функцію LastError () для надання детальної інформації про фактичну помилку.


2

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

Як

Існує 3 способи повернення інформації з функції:

  1. Повернене значення
  2. Зовнішні аргументи
  3. Поза діапазоном, що включає не локальні 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

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
}

Дійсно, але це не C, це може бути надано ОС чи ні. Якщо ви працюєте в операційних системах реального часу, наприклад, у вас їх немає.
Ілля

1

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


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. Список літератури: один , два .
Ant_222

0

На додаток до інших чудових відповідей, я пропоную вам спробувати відокремити прапор помилки та код помилки, щоб зберегти один рядок на кожному виклику, тобто:

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

Коли у вас багато перевірки помилок, це невелике спрощення дійсно допомагає.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.