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





Я лично предпочитаю первый подход (возврат индикатора ошибки).
В случае необходимости результат возврата должен просто указывать на то, что произошла ошибка, а другая функция используется для определения точной ошибки.
В вашем примере с getSize () я бы посчитал, что размеры всегда должны быть нулевыми или положительными, поэтому возврат отрицательного результата может указывать на ошибку, как это делают системные вызовы UNIX.
Я не могу вспомнить какую-либо библиотеку, которую я использовал, которая подходила бы для последнего подхода с объектом ошибки, переданным в качестве указателя. stdio и т. д. Все идут с возвращаемым значением.
Для справки, одна из библиотек, использующих последний подход, - это программный API Maya. Это скорее библиотека C++, чем C. Он довольно непоследователен в том, как он обрабатывает свои ошибки, и иногда ошибка передается как возвращаемое значение, а иногда он передает результат как ссылку.
не забывайте strtod, хорошо, последний аргумент не только указывает на ошибки, но и делает это тоже.
В прошлом я много занимался программированием на C. И я действительно оценил возвращаемое значение кода ошибки. Но есть несколько возможных подводных камней:
Вторая проблема может быть решена правильным уровнем предупреждений компилятора, правильным механизмом проверки кода и инструментами статического анализатора кода.
Также можно работать по принципу: если вызывается функция API, а возвращаемое значение не проверяется, значит, есть ошибка.
Мне нравится ошибка как способ возврата. Если вы разрабатываете api и хотите максимально безболезненно использовать свою библиотеку, подумайте об этих дополнениях:
сохраните все возможные состояния ошибок в одном перечислении с заданным типом и используйте его в своей библиотеке. Не нужно просто возвращать целые числа или, что еще хуже, смешивать целые числа или различные перечисления с кодами возврата.
предоставить функцию, которая преобразует ошибки в нечто удобочитаемое. Может быть просто. Просто перечисление ошибок, const char * out.
Я знаю, что эта идея несколько затрудняет использование многопоточности, но было бы неплохо, если бы прикладной программист мог установить глобальный обратный вызов ошибки. Таким образом они смогут поставить точку останова в обратном вызове во время сеансов поиска ошибок.
Надеюсь, поможет.
Почему вы говорите: «эта идея несколько затрудняет использование многопоточности». Какая часть усложняется многопоточностью? Вы можете привести небольшой пример?
@crypticcoder Проще говоря: обратный вызов глобальной ошибки может быть вызван в любом контексте потока. Если вы просто распечатаете ошибку, у вас не возникнет никаких проблем. Если вы попытаетесь исправить проблемы, вам нужно будет выяснить, какой вызывающий поток вызвал ошибку, и это усложняет задачу.
Что, если вы хотите сообщить более подробную информацию об ошибке? Например. у вас возникла ошибка парсера, и вы хотите указать номер строки и столбец синтаксической ошибки, а также способ красиво все это распечатать.
@panzi, тогда вам, очевидно, нужно вернуть структуру (или использовать указатель out, если структура действительно большая) и иметь функцию для форматирования структуры как строки.
Я демонстрирую ваши первые 2 маркера в коде здесь: stackoverflow.com/questions/385975/error-handling-in-c-code/…
Как этот подход справляется с функциями, которые естественным образом возвращают целые числа как логические значения? Например, проверить наличие чего-либо и затем использовать это в операторе if, например if (exists(my_item)) { ... }?
Я определенно предпочитаю первое решение:
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 не может быть вызвана для правильной обработки ошибок.
Первый подход лучше ИМХО:
Подход UNIX больше всего похож на ваше второе предложение. Вернуть либо результат, либо одно значение «все пошло не так». Например, open вернет дескриптор файла в случае успеха или -1 в случае неудачи. В случае сбоя он также устанавливает errno, внешнее глобальное целое число, указывающее, что произошел сбой который.
Как бы то ни было, Cocoa также применяет аналогичный подход. Некоторые методы возвращают BOOL и принимают параметр NSError **, поэтому в случае сбоя они устанавливают ошибку и возвращают NO. Тогда обработка ошибок выглядит так:
NSError *error = nil;
if ([myThing doThingError: &error] == NO)
{
// error handling
}
что находится где-то между двумя вашими вариантами :-).
В дополнение к тому, что было сказано, перед возвратом кода ошибки активируйте подтверждение или аналогичную диагностику при возвращении ошибки, так как это значительно упростит отслеживание. Я делаю это с помощью настраиваемого утверждения, которое по-прежнему компилируется при выпуске, но запускается только тогда, когда программное обеспечение находится в режиме диагностики, с возможностью беззвучного отчета в файле журнала или паузы на экране.
Я лично возвращаю коды ошибок как отрицательные целые числа с Нет ошибок равным нулю, но это оставляет вас с возможной следующей ошибкой
if (MyFunc())
DoSomething();
Альтернативный вариант - всегда возвращать ошибку как ноль и использовать функцию LastError (), чтобы предоставить подробную информацию о фактической ошибке.
Обновлено: если вам нужен доступ только к последней ошибке, и вы не работаете в многопоточной среде.
Вы можете вернуть только 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, он может быть предоставлен ОС или нет. Если вы работаете в операционных системах реального времени, например, у вас его не будет.
Во многих средах есть специальное положение, согласно которому псевдоглобальный getLastError() должен быть специфичным для текущего потока.
Второй подход позволяет компилятору создавать более оптимизированный код, потому что, когда адрес переменной передается функции, компилятор не может сохранять ее значение в регистрах во время последующих вызовов других функций. Код завершения обычно используется только один раз, сразу после вызова, тогда как «настоящие» данные, возвращенные из вызова, могут использоваться чаще.
Я использовал оба подхода, и оба они у меня отлично сработали. Какую бы из них я ни использовал, я всегда стараюсь применять такой принцип:
Если единственными возможными ошибками являются ошибки программиста, не возвращайте код ошибки, используйте утверждения внутри функции.
Утверждение, которое проверяет входные данные, ясно сообщает, что ожидает функция, в то время как слишком частая проверка ошибок может скрыть логику программы. Решение, что делать для всех различных случаев ошибок, действительно может усложнить дизайн. Зачем выяснять, как functionX должен обрабатывать нулевой указатель, если вместо этого вы можете настаивать на том, чтобы программист никогда не передавал его?
Есть пример утверждений на C? (Я очень зеленый до C)
Обычно это так же просто, как assert(X), где X - любое допустимое выражение C, которое вы хотите, чтобы оно было истинным. см. stackoverflow.com/q/1571340/10396.
Ух, абсолютно никогда не используйте утверждения в коде библиотеки! Кроме того, не смешивайте разные стили обработки ошибок в одном фрагменте кода, как и другие…
Я, конечно, согласен не смешивать стили. Мне любопытно, как вы рассуждаете об утверждениях. Если в моей документации по функциям говорится, что «аргумент X не должен быть NULL» или «Y должен быть членом этого перечисления», то что не так с assert(X!=NULL); или assert(Y<enumtype_MAX);? См. этот ответ на программистов и вопрос, на который он ссылается, для получения более подробной информации о том, почему я думаю, что это правильный путь.
+1 Различение между ошибками программирования, которые необходимо исправить, и ошибками времени выполнения, которые могут произойти всегда и с которыми необходимо работать, имеет такой смысл сейчас, когда я начал делать это из-за вас.
@AShelly Проблема с утверждениями, которые обычно отсутствуют в сборках релизов.
Я использую первый подход всякий раз, когда создаю библиотеку. Использование перечисления с определением типа в качестве кода возврата дает несколько преимуществ.
Если функция возвращает более сложный вывод, такой как массив и его длина, вам не нужно создавать произвольные структуры для возврата.
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 enum также позволяет отображать имя перечисления в отладчике. Это позволяет упростить отладку без необходимости постоянно обращаться к файлу заголовка. Также полезно иметь функцию для перевода этого перечисления в строку.
Самый важный вопрос, независимо от используемого подхода, - быть последовательным. Это относится к именованию функций и аргументов, порядку аргументов и обработке ошибок.
Я недавно размышлял над этой проблемой и написал некоторые макросы для C, имитирующие семантику try-catch-finally, используя чисто локальные возвращаемые значения. Надеюсь, вы найдете ее полезной.
Спасибо, что дали мне знать. Было интересно посмотреть.
Используйте 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;
}
Второй блок кода основан на более ранней версии кода в Страница Франческо Нидито, упомянутой в верхней части ответа. Код ETRY был изменен с момента написания этого ответа.
Setjmp - ужасная стратегия обработки ошибок. Это дорого, подвержено ошибкам (с энергонезависимыми измененными локальными переменными, не сохраняющими свои измененные значения и все такое) и приводит к утечке ресурсов, если вы выделяете какие-либо ресурсы между вызовами setjmp и longjmp. Вы должны быть в состоянии сделать примерно 30 возвратов и проверок int-val, прежде чем вы возместите стоимость sigjmp / longjmp. Большинство стеков вызовов не заходят так глубоко, особенно если вы не слишком сильно используете рекурсию (и если вы это сделаете, у вас будут проблемы с производительностью, кроме стоимости возвратов + проверок).
Если вы распределили память, а затем выбросили, память просто утекнет навсегда. Кроме того, setjmp стоит дорого, даже если не возникает никаких ошибок, он потребляет довольно много времени процессора и места в стеке. При использовании gcc для Windows вы можете выбирать между различными методами обработки исключений для C++, один из которых основан на setjmp, что на практике делает ваш код на 30% медленнее.
Когда я пишу программы, во время инициализации я обычно выделяю поток для обработки ошибок и инициализирую специальную структуру для ошибок, включая блокировку. Затем, когда я обнаруживаю ошибку с помощью возвращаемых значений, я ввожу информацию из исключения в структуру и отправляю SIGIO в поток обработки исключений, а затем смотрю, не могу ли я продолжить выполнение. Если я не могу, я отправляю SIGURG потоку исключения, который корректно останавливает программу.
Я предпочитаю обработку ошибок в 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
Вот подход, который я считаю интересным, но требующим некоторой дисциплины.
Это предполагает, что переменная типа дескриптора является экземпляром, на котором работают все функции 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;
}
Возврат кода ошибки - это обычный подход к обработке ошибок в C.
Но недавно мы также экспериментировали с подходом исходящего указателя ошибок.
Он имеет некоторые преимущества перед подходом с возвращаемым значением:
Вы можете использовать возвращаемое значение для более значимых целей.
Необходимость записать этот параметр ошибки напоминает вам, что нужно обработать ошибку или распространить ее. (Вы никогда не забудете проверить возвращаемое значение fclose, не так ли?)
Если вы используете указатель ошибки, вы можете передавать его при вызове функций. Если его задает какая-либо из функций, значение не потеряно.
Установив точку останова по данным для переменной ошибки, вы можете определить, где именно возникла ошибка. Установив условную точку останова, вы также можете отловить определенные ошибки.
Это упрощает автоматизацию проверки, все ли ошибки вы обрабатываете. Соглашение о коде может заставить вас называть указатель ошибки как err, и он должен быть последним аргументом. Таким образом, сценарий может сопоставить строку err);, а затем проверить, следует ли за ней if (*err. Фактически на практике мы сделали макрос под названием CER (проверка возврата ошибки) и CEG (проверка ошибки перехода). Таким образом, вам не нужно вводить его всегда, когда мы просто хотим вернуть ошибку, и это может уменьшить визуальный беспорядок.
Однако не все функции в нашем коде имеют этот исходящий параметр. Этот исходящий параметр используется в случаях, когда вы обычно генерируете исключение.
Есть хороший набор слайдов от CMU CERT с рекомендациями, когда использовать каждый из распространенных методов обработки ошибок C (и C++). Один из лучших слайдов - это дерево решений:
Я бы лично изменил две вещи в этой тележке.
Во-первых, я хотел бы пояснить, что иногда объекты должны использовать возвращаемые значения для обозначения ошибок. Если функция только извлекает данные из объекта, но не изменяет объект, то целостность самого объекта не подвергается риску, и более уместно указывать ошибки с помощью возвращаемого значения.
Во-вторых, всегда не подходит для использования исключений в C++. Исключения хороши тем, что они могут уменьшить объем исходного кода, посвященного обработке ошибок, они в основном не влияют на сигнатуры функций и очень гибки в том, какие данные они могут передавать по стеку вызовов. С другой стороны, исключения могут быть неправильным выбором по нескольким причинам:
У исключений C++ очень специфическая семантика. Если вам не нужна такая семантика, то исключения C++ - плохой выбор. Исключение должно обрабатываться сразу после того, как оно было выброшено, и дизайн поддерживает тот случай, когда из-за ошибки потребуется раскрутить стек вызовов на несколько уровней.
Функции C++, которые генерируют исключения, не могут позже быть обернуты, чтобы не генерировать исключения, по крайней мере, без полной оплаты исключений в любом случае. Функции, возвращающие коды ошибок, можно обернуть для создания исключений C++, что сделает их более гибкими. new C++ делает это правильно, предоставляя вариант без метания.
Исключения C++ относительно дороги, но этот недостаток в основном преувеличен для программ, разумно использующих исключения. Программа просто не должна генерировать исключения в пути кода, где производительность является проблемой. На самом деле не имеет значения, насколько быстро ваша программа может сообщить об ошибке и выйти.
Иногда исключения C++ недоступны. Либо они буквально недоступны в реализации на C++, либо правила кода запрещают их.
Поскольку исходный вопрос касался многопоточного контекста, я думаю, что метод индикатора локальной ошибки (описанный в отвечатьSirDarius) был недооценен в исходных ответах. Он потокобезопасен, не заставляет вызывающего абонента немедленно обрабатывать ошибку и может объединять произвольные данные, описывающие ошибку. Обратной стороной является то, что он должен удерживаться объектом (или, я полагаю, каким-то образом связан внешне), и, возможно, его легче игнорировать, чем код возврата.
Вы могли заметить, что стандарты кодирования Google C++ по-прежнему говорят Мы не используем исключения C++.
Что вы могли бы сделать вместо того, чтобы возвращать свою ошибку и, таким образом, запрещая вам возвращать данные с вашей функцией, использует обертка для вашего типа возврата:
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 в обоих методах, которые вы представили).
В дополнение к другим отличным ответам я предлагаю вам попытаться разделить флаг ошибки и код ошибки, чтобы сохранить одну строку при каждом вызове, то есть:
if ( !doit(a, b, c, &errcode) )
{ (* handle *)
(* thine *)
(* error *)
}
Когда у вас много проверок ошибок, это небольшое упрощение действительно помогает.
Это также упрощает запись цепочек if (…) { … } else if (…) { … } else { … }.
Возможно, но я никогда не использую эти цепочки, предпочитая что-то менее похожее на червя и более структурированное, например, break в цикле, goto для следующего абзаца кода. Цепи if-else работают, когда каждое звено использует только ветвь then, а ветвь else содержит остальные тесты. Помещать обработку ошибок в такую цепочку мне кажется довольно неуклюжим.
Я встречался с этими вопросами и ответами несколько раз и хотел дать более исчерпывающий ответ. Я думаю, что лучший способ подумать об этом - это как, чтобы возвращать ошибки вызывающей стороне, и какие, которые вы возвращаете.
Есть 3 способа вернуть информацию из функции:
Вы можете вернуть значение только одного объекта, однако это может быть произвольный комплекс. Вот пример функции, возвращающей ошибку:
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.
Индикатор ошибки сообщает вам только о наличии проблемы, но ничего не говорит о ее характере:
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]);
}
Вместо того, чтобы предварительно выделять массив ошибок, вы также можете (пере) выделять его динамически по мере необходимости, конечно.
Обратный вызов - это самый эффективный способ обработки ошибок, поскольку вы можете указать функции, какое поведение вы хотели бы видеть, когда что-то пойдет не так. Аргумент обратного вызова может быть добавлен к каждой функции или, если настройка u требуется только для каждого экземпляра структуры, подобной этой:
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();
Одним интересным преимуществом обратного вызова является то, что он может быть вызван несколько раз или вообще не вызываться при отсутствии ошибок, при которых нет накладных расходов на счастливый путь.
Однако есть инверсия контроля. Вызывающий код не знает, был ли вызван обратный вызов. Таким образом, может иметь смысл также использовать индикатор.
Вот простая программа для демонстрации первых двух маркеров Ответ Нильса Пипенбринка здесь.
Его первые 2 пули:
store all possible error-states in one typedef'ed enum and use it in your lib. Don't just return ints or even worse, mix ints or different enumerations with return-codes.
provide a function that converts errors into something human readable. Can be simple. Just error-enum in, const char* out.
Предположим, вы написали модуль с именем mymodule. Сначала в mymodule.h вы определяете свои коды ошибок на основе перечисления и пишете несколько строк ошибок, которые соответствуют этим кодам. Здесь я использую массив строк C (char *), который хорошо работает только в том случае, если ваш первый код ошибки на основе перечисления имеет значение 0, и вы не манипулируете числами после этого. Если вы используете номера кодов ошибок с пробелами или другими начальными значениями, вам просто придется перейти от использования сопоставленного массива C-строк (как я делаю ниже) на использование функции, которая использует оператор switch или операторы if / else if для сопоставления кодов ошибок перечисления с печатаемыми строками 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;
}
Выход:
Demonstration of enum-based error codes in C (or C++)
err code from mymodule_func1() = MYMODULE_ERROR_OK
err code from mymodule_func2() = MYMODULE_ERROR_INVARG
err code from mymodule_func3() = MYMODULE_ERROR_MYERROR
Вы можете запустить этот код самостоятельно здесь: https://onlinegdb.com/ByEbKLupS.
Если вы хотите, чтобы ваша программа аварийно завершила работу и не знаете причину, то доверяйте программистам и основам обработки ошибок.
Я думаю, что лучше всего создать какой-то отчет об ошибках, называть его режимом отладки, выключать, когда вам нужна максимальная производительность, и включать, когда вы хотите отладить проблему. Надеюсь, ты сможешь ударить его снова.
Будут ошибки, вопрос в том, как вы хотите проводить дни и ночи в их поисках.
Голосование против обвинения базовой обработки ошибок C и доверия со стороны других программистов - что, как я понимаю, означает доверие к флагам или кодам ошибок в их API - в снижении надежности программы. Например, если malloc() сообщает о сбое, вы должны доверять ему и никогда не пытаться перепроверить его альтернативными способами.
Я изучаю C всего около двух недель, но у меня возникло ощущение, что параметры OUT являются возвращаемым значением defacto для большинства функций, поскольку они позволяют избежать накладных расходов на возврат структур по значению и смягчают последствия. необходимость освободить память, так как большинство переменных находятся в стеке. Так как я не использую «возврат» для фактического значения функции, я могу использовать его для обработки ошибок большую часть времени.