确定错误的原始原因

时间:2017-10-02 15:52:26

标签: c error-handling

在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的事实。

是否有支持此类内容的库?

4 个答案:

答案 0 :(得分:6)

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

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

e.g。

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

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

答案 1 :(得分:3)

每个函数都可能有自己独立,记录和隔离的错误集。就像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的呼叫站点。

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

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第4个错误。

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

答案 2 :(得分:2)

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

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();
    }
}
祝你好运!

答案 3 :(得分:1)

这不是一个完整的解决方案,但我倾向于让每个编译单元(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
}