【问题标题】:在 C 中进行清理的非本地退出的最佳实践?
【发布时间】:2022-01-23 11:53:14
【问题描述】:

什么是中止 C 中错误的最佳实践?

在我们的代码库中,我们目前有一个使用模式

#define CHECKERROR(code) if(code) { return code; }

但这会导致资源没有在表单的代码中关闭

/* not actual code due to non-disclosure restrictions */
int somefunction() {
    handle_t res1, res2;
    int errorcode;

    res1 = getResource();
    res2 = getResource();

    errorcode = action1(res1, res2);
    CHECK(errorcode);
    errorcode = action2(res1, res2);
    CHECK(errorcode);

    freeResource(res1);
    freeResource(res2);
    return errorcode;
}

我遇到了这种模式

/* initialize resources */
do {
    /* ... */
    errorcode = action();
    if(errorcode) break;
    /* ... */
} while(0);
/* cleanup resources */
return errorcode;

以前在文章中,但现在找不到任何讨论它的来源。

什么是 C 语言的惯用做法? do { } while(0); 模式是否符合条件?有没有一种惯用的方式让它更清楚,它不是打算成为一个循环,而是一个具有非本地退出的块?

【问题讨论】:

  • 我喜欢这个问题,但正如目前所问的那样,它会征求意见,并且以目前的形式,它不会在 SO 上长时间保持开放状态。迁移到 SO 需要您更新问题。
  • 我不确定它是否是惯用的,但我不知道有比 do { ... } while (0) 构造更好的解决方案,它真的可以让你跳跃(阅读 break转到)错误处理代码。
  • 没有最佳实践。你可以试试“cleanup”属性
  • 有一个提议将“延迟”机制添加到 C23 以某种方式标准化错误处理。看看gustedt.wordpress.com/2020/12/14/a-defer-mechanism-for-c
  • 你考虑过使用atexit()吗?

标签: c resource-management


【解决方案1】:

什么是中止 C 中错误的最佳实践?

什么是 C 语言的惯用做法?

真的,没什么。没有最佳实践。最好是针对您正在处理的特定案例量身定制特定的解决方案。当然 - 专注于编写可读的代码。

让我们提到一些文件。 MISRA 2008 有以下内容。规则很严格 - single 退出点。所以你必须分配变量并跳转到单个return 语句

规则 6–6–5(必需)函数应在 函数结束。

错误处理是唯一真正鼓励使用goto 的地方。 Linux 内核编码风格呈现并鼓励使用goto 来“保持所有退出点关闭”。该样式未强制执行 - 并非所有内核函数都使用此样式。见Linux kernel coding style # Centralized exiting of functions

goto的内核推荐被SEI-C采纳:MEM12-C. Consider using a goto chain when leaving a function on error when using and releasing resources

do { } while(0);模式符合条件?

当然,为什么不呢。如果你没有在do { .. here .. }while(0)块内分配更多的资源,你不妨编写一个单独的函数,然后从中调用return

这个想法也有扩展。甚至使用 longjmp 在 C 中实现异常。我知道ThrowTheSwitch/CException

总的来说,C 语言中的错误处理并不容易。处理来自多个库的错误变得非常困难,并且是一门艺术。见MBed OS error-handlingmbed_error.h,甚至a site that explains MBed OS error codes

强烈希望您的函数使用单个返回点 - 正如您发现的那样,使用您的 CHECK(errorcode); 会泄漏资源。多个return 地点令人困惑。考虑使用gotos:

int somefunction() {
    int errorcode = 0;

    handle_t res1 = getResource();
    if (!res1) {
        errorcode = somethnig;
        goto res1_fail;
    }
    handle_t res2 = getResource();
    if (!res2) {
        errorcode = somethnig_else;
        goto res2_fail;
    }

    errorcode = action1(res1, res2);
    if (!errorcode) {
       goto actions_fail;
    }
    errorcode = action2(res1, res2);
    if (!errorcode) {
       goto actions_fail;
    }

actions_fail:
    freeResource(res2);
res2_fail:
    freeResource(res1);
res1_fail:
    return errorcode;
}

【讨论】:

  • 关于 MISRA-C 和多次退货,请参阅我的答案中的链接。另请注意,MISRA-C:2004 和 MISRA-C++:2008 都禁止goto。但是 MISRA-C:2012 允许使用 goto,如您的代码示例所示。
  • 此解决方案令人烦恼。资源/获取和释放在空间上是完全分开的。这对于长链来说很麻烦。此外,排序很重要,因为在重构链中间的东西时很容易破坏。
【解决方案2】:

首先,隐藏流控制的神秘宏(例如 CHECKERROR)被广泛认为是非常糟糕的做法。不要那样做——创建其他 C 程序员不理解的秘密宏语言是比代码重复更严重的质量问题。代码重复不好,但不应该通过制造更糟糕的问题来解决。假设读者非常了解 C,但不要假设他们知道或想知道该项目本地的秘密宏语言。

在惯用的 C 语言中,有两种可接受的方式来编写此代码。使用显式return 或使用“on error goto”模式à la BASIC。我通常会推荐return 版本,因为它可以让您避免再次进行那种“goto 被认为有害”的旧辩论。但是在函数结束时转到清理也是可以接受的,只要你只向下跳转。

(您的do-while(0)break 只是伪装的goto。没有好坏之分。)

函数中return 的单点也存在争议,尤其是在 MISRA-C 的上下文中(请参阅this)。然而,函数的多次返回是fine,只要它不会使代码更难阅读。实际上,这意味着您应该避免在深度嵌套的循环或语句中使用return(或goto)。一般尽量保持“圈复杂度”(一个函数中可能执行路径的数量)尽可能低。

如果您需要释放资源,我个人更喜欢return 而不是goto。对于return,您需要制作一个包装函数,该函数也用于将资源分配与算法分离。我会像这样重写你的代码:

typedef enum // use an actual enum not sloppy int
{
  OK, // keeping code 0 for no error is the most common practice
  ERR_THIS,
  ERR_THAT
} err_t;

static err_t the_actual_algorithm (handle_t res1, handle_t res2) // likely inlined
{
   err_t errorcode;

   errorcode = action1(res1, res2);
   if(errorcode != OK) { return errorcode; }

   errorcode = action2(res1, res2);
   if(errorcode != OK) { return errorcode; }

   return OK;
}

err_t somefunction (void)  // note void, not empty parenthesis which is obsolete style
{
    handle_t res1, res2;
    err_t errorcode;

    res1 = getResource();
    res2 = getResource();

    errorcode = the_actual_algorithm(res1, res2);
 
    freeResource(res1);
    freeResource(res2);
    return errorcode;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-04-16
    • 1970-01-01
    • 2010-10-11
    • 2010-12-19
    • 1970-01-01
    • 1970-01-01
    • 2013-01-23
    • 1970-01-01
    相关资源
    最近更新 更多