【问题标题】:determining original cause of error确定错误的原始原因
【发布时间】:2017-10-02 15:52:25
【问题描述】:

C 中的嵌套错误处理是否有一些众所周知的模式/实践,例如 Java 中的嵌套异常?

通常的“只返回错误代码/成功”错误详细信息可能会在程序确定它应该记录/报告错误之前丢失。

想象一个类似这样的代码:

err B()
{
  if (read(a/b/c/U.user) != OK) {
    return read_error; //which would be eaccess or we could return even e_cannot_read_user
  }

  if (is_empty(read_user.name)) {
    // we could tell exactly what is missing here
    return einval;
  }
  ...
}

err A()
{
  if (B() != OK) {
    if (cannot_handle_B_failing()) {
      return e_could_not_do_b;
    }
  }
  ...
}

main()
{
  ...
  if (A() != OK) && (no_alternative_solution()) {
    report error_returned_by_A;
    wait_for_more_user_input();
  }
}

有没有人在这种情况下成功地尝试过在 C 中使用某种嵌套的错误代码/消息?可以报​​告(主要)用户名丢失或由于权限无效而无法读取文件 F 的事实。

是否有支持此类功能的库?

【问题讨论】:

  • 我见过的一种方法 - 您使用 32 位 int 报告错误 - 但将其拆分为范围,低 16 位是错误代码 - 由较低层函数设置,接下来的 8 位保留用于中间层函数和最高 8 位保留供顶层函数使用。然后从 B 收到的错误是 |= 在带有附加信息的 A 中
  • 简短的回答是否定的。 C 是编程语言的白板。很少有广泛使用的习惯用法来实现大型语言中的标准特性:容器、异常、泛型等。(已经给出的答案的多样性是某种证明。)在 Java 异常子系统中,您会收到一个链表的原因。在 C 中,您当然可以自己编写:返回指向原因结构链的指针,而不是整数错误代码。如果你想要类似异常的行为,你可以使用 setjmplongjmp 来拼凑它,而不会有太多麻烦。
  • 我知道 C 语言是“发明你自己的轮子”的语言。当然,您可以自己编写“原因列表”,但您还必须在处理错误时清理任何动态分配的跟踪元素,这当然是可行的(例如,保留一些内部分配器并重用“原因”或在新的时候清理它们错误是“创建”)。想知道是否有一个库可以比几个小时内编写的解决方案更好地解决这个问题,这是非常重要的......
  • 感谢所有响应者的输入。考虑到答案,结论是没有明确的最佳方法来处理错误,需要逐案决定。
  • @dbrank0,在编码 C 20 年后遇到这个问题(我不是很聪明,第一年我认为我什至没有看到这个问题)我遇到了一些我的项目中的一种平衡,因为这个“确定失败的原因”:永远不会好,永远不会糟糕。我的意思是我像你说的那样逐个查看它,并得出一些可行的方法,它永远不会完全适合,但总是“好吧,这看起来有点尴尬,但它会足够公平".也许有人会找到一个很好的方法来做到这一点。或者不。

标签: c error-handling


【解决方案1】:

我建议你看看Apple's error handling guideline。它是为 Objective-C 设计的,主类是NSError。他们使用userInfo 字典(映射)来保存有关错误的详细信息,并且如果需要,他们已经预定义了NSUnderlyingErrorKey 常量来保存该字典中的底层NSError 对象。

因此您可以为您的代码声明自己的错误struct 并实施类似的解决方案。

例如

typedef struct {
  int code;
  struct Error *underlyingError;
  char domain[0];
} Error;

然后您可以使用domain 字段对错误进行分类(根据需要按库、文件或函数); code 字段用于确定错误本身,可选underlyingError 字段用于找出导致您收到错误的潜在错误。

【讨论】:

  • 谢谢。好来源。虽然 C 中的簿记 Error struct (copy, destruct) 很烂。
  • 否则,您需要为每个错误组合声明一个常量(>70% 的源代码将成为纯错误处理代码),或者将您的错误作为二进制掩码存储在一个变量中,您将拥有如前所述,具有一定的灵活性。但是无论如何,嵌套函数的深度都会受到限制(即,如果您有 20 个嵌套函数调用,则无法将错误从最深的函数传递给调用者)。
【解决方案2】:

每个函数都可能有自己独立的、记录在案的、孤立的错误集。就像 libc 中的每个函数都有自己记录的可能返回值和 ERRNO 代码集。

“根本原因”只是一个实现细节,你只需要知道它失败的“原因”。

换句话说,A 的文档不应该解释 B,不应该告诉它使用 B,也不应该告诉 B 的错误代码,它可以有自己的、本地有意义的错误代码。

此外,在尝试替代方案时,您必须保留原始故障代码(本地),因此如果替代方案也失败了,您仍然能够知道是什么导致您首先尝试它们。

err B()
{
  if (read(a/b/c/U.user) != OK) {
    return read_error; //which would be eaccess or we could return even e_cannot_read_user
  }

  if (is_empty(read_user.name)) {
    // we could tell exactly what is missing here
    return einval;
  }
  ...
}

err A()
{
  if ((b_result = B()) != OK) {
    // Here we understand b_result as we know B,
    // but outside of we will no longer understand it.
    // It means that we have to map B errors
    // to semantically meaningful A errors.
    if (cannot_handle_B_failing()) {
      if (b_result == …)
          return e_could_not_do_b_due_to_…;
      else if (b_result == …)
          return e_could_not_do_b_due_to_…;
      else
          return e_could_not_do_b_dont_know_why;

    }
  }
  ...
}

main()
{
  ...
  if ((a_result = A()) != OK) && (no_alternative_solution()) {
    // Here, if A change its implementation by no longer calling B
    // we don't care, it'll still work.
    report a_result;
    wait_for_more_user_input();
  }
}

将 B 的错误映射到 A 的错误的成本很高,但有好处:当 B 更改其实现时,它不会破坏 A 的所有调用站点。

这种语义映射起初可能看起来毫无用处(“我会将“权限被拒绝”映射到“权限被拒绝”...),但必须适应当前的抽象级别,通常来自“无法打开”文件”改为“无法打开配置”,例如:

err synchronize(source, dest, conf) {
    conf_file = open(conf);
    if (conf == -1)
    {
       if (errno == EACCESS)
           return cannot_acces_config;
       else
           return unexpected_error_opening_config_file;
    }
    if (parse(config_file, &config_struct) == -1)
        return cannot_parse_config;
    source_file = open(source);
    if (source_file == -1)
    {
       if (errno == EACCESS)
           return cannot_open_source_file;
       else
           return unexpected_error_opening_source_file;
    }
    dest_file = open(dest);
    if (dest == -1)
    {
       if (errno == EACCESS)
           return cannot_open_dest_file;
       else
           return unexpected_error_opening_dest_file;
    }
}

它不一定是一对一的映射。如果您一对一映射错误,对于三个函数的深度,每个调用三个调用,更深的函数有 16 个不同的可能错误,它将映射到 16 * 3 * 3 = 144 个不同的不同错误,即只是每个人的维护地狱(想象一下您的翻译人员也必须翻译 144 条错误消息......并且您的文档列出并解释了所有这些,对于一个单一的功能)。

所以,不要忘记函数必须抽象他们正在做的工作以及他们遇到的错误抽象,到一个可理解的、本地有意义的、一组错误。

最后,在某些情况下,即使保留对所发生事件的完整堆栈跟踪,您也无法推断出错误的根本原因:假设配置读取器必须在 5 个不同的位置查找配置,它可能会遇到3个“找不到文件”,一个“权限被拒绝”,另一个“找不到文件”,所以它会返回“找不到配置”。从这里,除了用户之外,没有人能说出失败的原因:也许用户在第一个文件名中输入了错字,而权限被拒绝是完全可以预料的,或者前三个文件不应该存在,但用户做了一个 chmod第四次出错。

在这些情况下,帮助用户调试问题的唯一方法是提供详细标志,例如“-v”、“-vv”、“-vvv”……每次添加新级别的调试详细信息,直到用户能够在日志中看到配置有 5 个要检查的位置、检查第一个位置、找不到文件等等,并推断出程序偏离其意图的位置。

【讨论】:

  • 听起来很合理,并且与我现在所做的类似,但似乎有很多重复,您很快就可以超过 previrbial 标准的 50% 代码,只是为了处理错误。保留整个“堆栈跟踪”实际上是可选的:您总是可以用其他东西替换它(比如抛出没有捕获的单独异常)。
  • 我同意,50% 的代码仅用于处理错误太多了,函数必须抽象它们所做的事情并报告本地有意义的错误。我编辑了我的答案以更好地反映这一点。
  • 我也同意。这是一种非常合理的方法,也是 POSIX 库处理错误的方式。这里的主要思想是错误表示违反合同。并且合同应该是模糊的,但具有相似的标准(即参数是否有效)。这是一种简单、独立的方法,可以说保持可读性。
【解决方案3】:

我们在其中一个项目中使用的解决方案是通过完整的函数堆栈传递特殊的错误处理结构。这允许在任何更高级别上获取原始错误和消息。使用此解决方案,您的示例将如下所示:

struct prj_error {
    int32_t err;
    char msg[ERR_MAX_LEN];
};

prj_error_set(struct prj_error *err, int errorno, const char *fmt, ...); /* implement yourselves */

int B(struct prj_error *err)
{
    char *file = "a/b/c/U.user";
    if (custom_read(file) != OK) {
        prj_error_set(err, errno, "Couldn't read file \"%s\". Error: %s\n",
            file, strerror(errno));
        return err->err;
    }

    if (is_empty(read_user.name)) {
        prj_error_set(err, -ENOENT, "Username in file \"%s\" is empty\n",
            file);
        return err->err;
    }
    ...
}

int A(struct prj_error *err)
{
    if (B(err) != OK) {
        if (cannot_handle_B_failing()) {
            return err.err;
        }
    }
    ...
}

main()
{
    struct prj_error err;
    ...
    if (A(&err) != OK) && (no_alternative_solution()) {
        printf("ERROR: %s (error code %d)\n", err.msg, err.err);
        wait_for_more_user_input();
    }
}

祝你好运!

【讨论】:

    【解决方案4】:

    这不是一个完整的解决方案,但我倾向于让每个编译单元(C 文件)都有唯一的返回码。它可能有几个外部可见的函数和一堆静态(仅在本地可见)的函数。

    那么在 C 文件中,返回值是唯一的。在 C 文件中,如果有意义,我还决定是否需要记录一些内容。无论返回什么,调用者都可以确切地知道出了什么问题。

    这些都不是很好。 OTOH 异常也有皱纹。当我用 C++ 编码时,我不会错过 C 的返回处理,但奇怪的是,当我用 C 编码时,我不能直截了当地说我错过了异常。它们以自己的方式增加了复杂性。

    我的程序可能如下所示:

    some_file.c:


    static int _internal_function_one_of_a_bunch(int h)
    {
       // blah code, blah
       if (tragedy_strikes()) {
           return 13;
       }
    
       // blah more code
       return 0; // OK
    }
    
    static int _internal_function_another(int h)
    {
       // blah code, blah
       if (tragedy_strikes_again()) {
           return 14;
       }
    
       if (knob_twitch() != SUPER_GOOD) {
           return 15;
       }
    
       // blah more code
       return 0; // OK
    }
    
    
    // publicly visible
    int do_important_stuff(int a)
    {
        if (flight_status() < NOT_EVEN_OK) {
            return 16;
        }
    
        return _internal_function_another(a) ||
               _internal_function_one_of_a_bunch(2 * a) ||
               0; // OK
    }
    

    【讨论】:

    • 是的,这是一种干净的方式,需要大量样板代码(将模块 A 的 A_EIO 转换为模块 B 的 B_EIO)并且容易出错(不要检查 B_EIO 以从 A 的函数返回)。此外,较低级别的函数有时应该,有时不应该记录失败。
    • @dbrank0,我同意,它充满了样板代码,而且不是很好。但这是我在大多数项目中实施的最佳点。它甚至不是很甜蜜,只是没有伤害那么...在我在 C 领域的所有旅行中,我还没有看到通用模式或解决方案 - 但也许我应该更加努力.我有一种预感,一个全局错误列表可能是可行的,但实际上它几乎需要自定义工具支持 - 当你走这条路时,你 (至少 I) i> 总是可能会想完全看其他语言。
    猜你喜欢
    • 1970-01-01
    • 2011-12-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-08-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多