Обрабатывать ошибки с помощью объединения - PullRequest
0 голосов
/ 26 декабря 2018

Я пришел на C с языка высокого уровня Scala и задал вопрос.В Scala мы обычно обрабатываем ошибки / исключительные условия, используя Либо , который выглядит следующим образом:

sealed abstract class Either[+A, +B] extends Product with Serializable 

Итак, грубо говоря, он представляет собой сумму типов A и B.Любой может содержать только один экземпляр (A или B) в любой момент времени.По соглашению A используется для ошибок, B для действительного значения.

Это выглядит очень похоже на union, но, поскольку я очень плохо знаком с C, я не уверен, еслиДля обработки ошибок довольно условно использовать объединения.

Я склонен сделать что-то вроде следующего для обработки ошибки дескриптора открытого файла:

enum type{
    left,
    right
};

union file_descriptor{
    const char* error_message;
    int file_descriptor;
};

struct either {
    const enum type type;
    const union file_descriptor fd;
};

struct either opened_file;
int fd = 1;
if(fd == -1){
    struct either tmp = {.type = left, .fd = {.error_message = "Unable to open file descriptor. Reason: File not found"}};
    memcpy(&opened_file, &tmp, sizeof(tmp));
} else {
    struct either tmp = {.type = right, .fd = {.file_descriptor = fd}};
    memcpy(&opened_file, &tmp, sizeof(tmp));
}

Но я 'Я не уверен, что это обычный способ С.

1 Ответ

0 голосов
/ 26 декабря 2018

Я не уверен, является ли условным использование объединения для обработки ошибок.

Нет, это не так.Я бы категорически против этого, потому что, как вы видите, он генерирует много кода для чего-то, что должно быть действительно простым.

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

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.

...