使用 union 处理错误

Handle errors with union

我是从高级Scala语言来到C的,想出了一道题。在 Scala 中,我们通常使用 Either 来处理 error/exceptional 条件,如下所示:

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

所以粗略地说它呈现了类型 AB 的总和。在任何给定时间,任一个都只能包含一个实例(AB)。按照惯例,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));
}

但我不确定这是否是传统的 C 方式。

I'm not sure if it is sort of conventional to make use of union for error handling.

不,不是。我强烈反对它,因为如您所见,它会为一些本应非常简单的东西生成大量代码。

有几种更常见的模式。当函数对结构体进行操作时,使用

更为常见
int operation(struct something *reference, ...);

它采用指向要操作的结构的指针,如果成功则 returns 0,否则为错误代码(或 -1 并设置 errno 以指示错误)。

如果函数 return 是一个指针,或者您需要一个接口来报告复杂的错误,您可以使用一个结构来描述您的错误,并让操作采用指向此类结构的额外指针:

typedef struct {
    int         errnum;
    const char *errmsg;
} errordesc;

struct foo *operation(..., errordesc *err);

通常,该操作仅在确实发生错误时修改错误结构;它没有清除它。这使您可以轻松地 "propagate" 跨越对原始调用者的多个函数调用级别的错误,尽管原始调用者必须先清除错误结构。

您会发现其中一种方法可以很好地映射到您希望为其创建绑定的任何其他语言。


OP 在评论链中提出了几个后续问题,我认为这些问题对其他程序员(尤其是那些用不同编程语言编写例程绑定的程序员)很有用,所以我想对 in 中的错误的实际处理进行一些详细说明订单。

关于错误首先要认识到的是,在实践中,我们将它们分为两类:可恢复,和不可恢复:

  • 可恢复的错误是那些可以忽略(或解决)的错误。

    例如,如果您有图形用户界面或游戏,并且在您尝试播放音频事件(例如,完成 "ping!")时发生错误,那显然不应该导致整个申请中止。

  • 不可恢复的错误是那些严重到足以保证应用程序(或服务守护程序中的每个客户端线程)退出的错误。

    例如,如果您有图形用户界面或游戏,并且在构建初始 window/screen 时内存不足,除了中止并记录错误外,它别无选择.

不幸的是,函数本身通常无法区分两者:由调用者做出决定。

因此,错误指示器的主要目的是为调用者提供足够的信息来做出决定。

第二个目的是为人类用户(和开发人员)提供足够的信息,以确定错误是软件问题(代码本身的错误)、硬件问题的指示还是其他原因否则。

例如,当使用 POSIX 低级别 I/O(read(), write()), the functions can be interrupted by the delivery of a signal to a signal handler installed without the SA_RESTART 标志使用该特定线程时。在这种情况下,该函数将 return 短计数(少于请求的数据 read/written),或 -1 与 errno == EINTR.

在大多数情况下,可以安全地忽略 EINTR 错误,并重复 read()/write() 调用。然而,在 POSIX C 中实现 I/O 超时的最简单方法就是使用那种中断。因此,如果我们编写忽略 EINTR 的 I/O 操作,它不会受到典型超时实现的影响;它将 block 或永远重复,直到它真正成功或失败。同样,函数本身无法知道 EINTR 错误是否应该被忽略;只有来电者知道。

在实践中,Linux errno or POSIX errno 值涵盖了绝大多数实际需求。 (这不是巧合;这个集合涵盖了 POSIX.1 标准 C 库函数可能发生的错误。)

在某些情况下,自定义错误代码或 "subtype" 标识符很有用。线性代数数学库可以为所有数学错误提供子类型编号,而不是仅仅 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 }

应该能满足我个人的需求。

我个人并不关心整个错误跟踪,因为根据我的经验,一切都可以追溯到最初的错误。 (换句话说,当某些东西发生 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 字段作为键;只有当整个结构处于 "no error" 状态时它才为零),它是重要的是不要覆盖现有的错误描述。这确保跨越多个函数调用的复杂操作可以使用单个此类错误结构,并且仅保留根本原因。

为了程序员的整洁,你甚至可以写一个预处理器宏,

#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.

的错误报告