Я не уверен, является ли условным использование объединения для обработки ошибок.
Нет, это не так.Я бы категорически против этого, потому что, как вы видите, он генерирует много кода для чего-то, что должно быть действительно простым.
Есть несколько гораздо более распространенных шаблонов.Когда функция работает со структурой, гораздо более распространенным является использование
int operation(struct something *reference, ...);
, которое принимает указатель на оперируемую структуру и возвращает 0 в случае успеха и код ошибки в противном случае (или -1 с errno
, установленным для обозначения ошибки).
Если функция возвращает указатель или вам нужен интерфейс для сообщения о сложных ошибках, вы можете использовать структуру для описания ваших ошибок и заставить операции выполнятьдополнительный указатель на такую структуру:
typedef struct {
int errnum;
const char *errmsg;
} errordesc;
struct foo *operation(..., errordesc *err);
Обычно операция модифицирует структуру ошибки только тогда, когда ошибка возникает;это не очищает это.Это позволяет вам легко «распространять» ошибки по нескольким уровням вызовов функций для исходного вызывающего, хотя исходный вызывающий должен сначала очистить структуру ошибок.
Вы обнаружите, что один из этих подходов сопоставляется с любымна другом языке, для которого вы хотите создать привязки, довольно красиво.
OP поставил несколько последующих вопросов в цепочке комментариев, которые, как я считаю, полезны для других программистов (особенно тех, которые пишут привязки для подпрограмм на разных языках программирования),поэтому я думаю немного проработать практическую обработку ошибок по порядку.
Первое, что нужно понять в отношении ошибок, это то, что на практике мы делим их на две категории: восстанавливаемые и неустранимо :
К исправимым ошибкам относятся те, которые можно игнорировать (или обойти).
Например, если у вас есть графический интерфейс пользователяили игра, и возникает ошибка при попытке воспроизвести звуковое событие (скажем, cисключение «ping!»), которое, очевидно, не должно вызывать прерывание всего приложения.
Неустранимые ошибки - это те ошибки, которые достаточно серьезны, чтобы гарантировать применение приложения (или потока на клиенте в демоне службы).) для выхода.
Например, если у вас есть графический пользовательский интерфейс или игра, и ему не хватает памяти при создании начального окна / экрана, ничего другого он не может сделать, но может прервать ирегистрировать ошибку.
К сожалению, сами функции обычно не могут различать два: решение должен принять вызывающий.
Следовательно, основная цель индикатора ошибки - предоставить вызывающей стороне достаточно информации для принятия этого решения.
Вторичной целью является предоставление достаточного количества информации пользователю (и разработчикам) для определенияявляется ли ошибка программной проблемой (ошибка в самом коде), указывает на аппаратную проблему или что-то еще.
Например, при использовании низкоуровневого ввода-вывода POSIX (read()
, write()
) функции могут быть прерваны при доставке сигнал для обработчика сигнала, установленного без флага SA_RESTART
, использующего этот конкретный поток.В этом случае функция вернет короткий счет (меньше, чем запрошенные данные для чтения / записи) или -1 с errno == EINTR
.
В большинстве случаев эту ошибку EINTR можно безопасно игнорировать, и чтение() / write () вызов повторен.Однако самый простой способ реализовать тайм-аут ввода-вывода в POSIX C - использовать именно такие прерывания.Таким образом, если мы напишем операцию ввода-вывода, которая игнорирует EINTR, на нее не повлияет типичная реализация тайм-аута;он будет блокировать или повторяться вечно, пока он на самом деле не преуспеет или не потерпит неудачу.Опять же, сама функция не может знать, следует ли игнорировать ошибки EINTR;это то, что знает только звонящий.
На практике значения Linux errno
или POSIX errno
охватывают подавляющее большинство практических потребностей.(Это не совпадение; этот набор охватывает ошибки, которые могут возникать при использовании стандартных библиотечных функций C с поддержкой POSIX.1.)
В некоторых случаях пользовательский код ошибки или идентификатор «подтипа»полезно.Вместо просто EDOM
для всех математических ошибок математическая библиотека линейной алгебры может иметь номера подтипов для ошибок, таких как размерность матрицы, не подходящая для умножения матрицы на матрицу и т. Д.
Для потребностей отладки человеком файлимя, имя функции и номер строки кода, который обнаружил ошибку, было бы очень полезно.К счастью, они предоставляются как __FILE__
, __func__
и __LINE__
соответственно.
Это означает, что структура, аналогичная
typedef struct {
const char *file;
const char *func;
unsigned int line;
int errnum; /* errno constant */
unsigned int suberr; /* subtype of errno, custom */
} errordesc;
#define ERRORDESC_INIT { NULL, NULL, 0, 0, 0 }
, должна покрывать потребности, которые я лично могуenvision.
Лично мне нет дела до всей трассировки ошибок, потому что по моему опыту все можно отследить до первоначальной ошибки.(Другими словами, когда что-то идет b0rk , множество других вещей тоже имеют тенденцию идти b0rk , причем только корень b0rk имеет значение. Другие могут не согласиться, но по моему опыту, случаи, когда необходима вся трассировка, лучше всего обрабатываются соответствующими средствами отладки, такими как трассировка стека и дампы ядра.)
Допустим, мы реализуем функцию, подобную открытию файла (возможно перегруженную,так что он может не только читать локальные файлы, но и полные URL-адреса?), который принимает параметр errordesc *err
, инициализированный вызывающей стороной на ERRORDESC_INIT
(поэтому указатели равны NULL, номер строки равен нулю, а номера ошибок равны нулю).В случае сбоя стандартной библиотечной функции (таким образом, устанавливается errno
), она регистрирует ошибку следующим образом:
if (err && !err->errnum) {
err->file = __FILE__;
err->func = __func__;
err->line = __LINE__;
err->errnum = errno;
err->suberr = /* error subtype number, or 0 */;
}
return (something that is not a valid return value);
Обратите внимание, как этот раздел позволяет вызывающей стороне передавать NULL
, если это действительно происходитне волнует ошибка вообще.(Я придерживаюсь мнения, что функции должны облегчать программистам обработку ошибок, но не пытаться принудительно их исправлять: глупые программисты более глупы, чем я могу себе представить, и просто сделают что-то еще более глупое, если я попытаюсь заставить ихделайте это менее глупо. Обучение камням прыгать более полезно, на самом деле.)
Кроме того, если структура ошибок уже заполнена (здесь я использую поле errnum
в качестве ключа; этоноль, только если вся структура находится в состоянии «без ошибок»), важно не перезаписывать существующее описание ошибки.Это гарантирует, что сложная операция, которая охватывает несколько вызовов функций, может использовать одну такую структуру ошибок и сохранять только основную причину.
Для аккуратности программиста вы даже можете написать макрос препроцессора,
#define ERRORDESC_SET(ptr, errnum_, suberr_) \
do { \
errordesc *const ptr_ = (ptr); \
const int err_ = (errnum_); \
const int sub_ = (suberr_); \
if (ptr_ && !ptr_->errnum) { \
ptr_->file = __FILE__; \
ptr_->func = __func__; \
ptr_->line = __LINE__; \
ptr_->errnum = err_; \
ptr_->suberr = sub_; \
} \
} while(0)
так, чтобы в случае ошибки функции, которая принимает параметр errordesc *err
, требовалась только одна строка, ERRORDESC_SET(err, errno, 0);
(заменяющая 0
подходящим номером под-ошибки), которая заботится об обновленииструктура ошибок.(Он написан так, чтобы вести себя точно так же, как вызов функции, поэтому он не должен вызывать удивительного поведения, даже если это макрос препроцессора.)
Конечно, также имеет смысл реализовать функцию, которая может сообщатьтакие ошибки в указанном потоке, обычно stderr
:
void errordesc_report(errordesc *err, FILE *to)
{
if (err && err->errnum && to) {
if (err->suberr)
fprintf(to, "%s: line %u: %s(): %s (%d).\n",
err->file, err->line, err->func,
strerror(err->errnum), err->suberr);
else
fprintf(to, "%s: line %u: %s(): %s.\n",
err->file, err->line, err->func, strerror(err->errnum));
}
}
, который выдает сообщения об ошибках типа foo.c: line 55: my_malloc(): Cannot allocate memory.