使用联合处理错误

时间:2018-12-26 10:47:54

标签: c scala unions either

我从高级Scala语言开始学习C,并提出了一个问题。在Scala中,我们通常使用Either处理错误/异常情况,如下所示:

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方法。

1 个答案:

答案 0 :(得分:5)

  

我不确定使用并集进行错误处理是否很常规。

不,不是。我强烈反对这样做,因为如您所见,它为应该非常简单的内容生成了大量代码。

还有几种更常见的模式。当功能在结构上运行时,使用起来更为常见

int operation(struct something *reference, ...);

接受指向要操作的结构的指针,如果成功则返回0,否则返回错误代码(或将errno设置为-1以指示错误)。

如果函数返回一个指针,或者您需要一个接口来报告复杂的错误,则可以使用一种结构来描述您的错误,并让操作为该结构增加一个额外的指针:

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

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

通常,该操作仅在确实发生错误时才修改错误结构。它不能清除它。尽管原始调用者必须首先清除错误结构,这使您可以轻松地在多个级别的函数调用中“传播”错误给原始调用者。

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


OP在注释链中提出了一些后续问题,我认为这些问题对其他程序员(特别是那些使用不同编程语言为例程编写绑定的程序员)有用,因此我认为应该按顺序对错误的实际处理进行详细说明。

首先要意识到的错误是,在实践中,我们将其分为两类:可恢复不可恢复

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

    例如,如果您具有图形用户界面或游戏,并且在尝试播放音频事件时发生错误(例如,完成“ ping!”),则显然不应导致整个应用程序中止

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

    例如,如果您具有图形用户界面或游戏,并且在构造初始窗口/屏幕时内存不足,则它可以明智地进行其他操作,但可以中止并记录错误。

不幸的是,函数本身通常不能在两者之间进行区分:取决于调用方来做出决定。

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

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

例如,当使用POSIX低级I / O(read()write())时,可以通过向安装了signal的信号处理程序中传递SA_RESTART来中断功能使用该特定线程的block标志。在这种情况下,该函数将返回一个短计数(少于请求的读/写数据),或者返回errno == EINTR -1。

在大多数情况下,可以安全地忽略该EINTR错误,并重复执行read()/ write()调用。但是,在POSIX C中实现I / O超时的最简单方法是使用这种中断。因此,如果我们编写一个忽略EINTR的I / O操作,它将不受典型的超时实现的影响。它会Linux errno或永远重复一次,直到它成功或失败为止。同样,该函数本身不知道是否应忽略EINTR错误。这是来电者所知道的。

实际上,POSIX errno或{{3}}的值涵盖了绝大多数实际需求。 (这不是巧合;该集合涵盖了具有POSIX.1的标准C库函数可能发生的错误。)

在某些情况下,自定义错误代码或“子类型”标识符非常有用。线性代数数学库不仅可以对所有数学错误进行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字段作为键;仅当整个结构都处于“无错误”状态时,它才为零),因此重要的是不要覆盖现有的错误描述。这样可以确保跨多个函数调用的复杂操作可以使用单个此类错误结构,并且仅保留根本原因。

为了使程序员整洁,您甚至可以编写预处理器宏,

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