在 C 中进行清理的非本地退出的最佳实践?

Best practices for non-local exit with cleanup in C?

在 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); 模式是否符合条件?有没有一种惯用的方法可以使它更清楚,它 不是 打算成为一个循环,而是一个具有非本地出口的块?

What is considered best practice for aborting on errors in C?

What is a good practice, that would be considered idiomatic to C?

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

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

Rule 6–6–5 (Required) A function shall have a single point of exit at the end of the function.

错误处理是唯一真正鼓励使用 goto 的地方。 Linux 内核编码风格提出并鼓励使用 goto 来“保持所有退出点关闭”。风格不是强制的——不是所有的内核函数都使用它。参见 Linux kernel coding style # Centralized exiting of functions

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

Does the do { } while(0); pattern qualify?

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

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

总的来说,C 中的错误处理并不容易。处理来自多个库的错误变得极其困难,并且本身就是一门艺术。参见 MBed OS error-handling, mbed_error.h, even a site that explains MBed OS error codes

强烈推荐您的函数中的单个 return 点 - 正如您发现的那样,使用您的 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;
}

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

在惯用的 C 中,有两种可接受的方式来编写此代码。使用显式 return 或使用 BASIC 中的“出错时转到”模式。我通常会推荐 return 版本,因为它可以让您避免再次进行令人厌烦的“goto 被认为有害”的争论。但是在函数末尾转到清理也是可以接受的,只要你只向下跳。

(你的do-while(0)break只是变相的goto,没有好坏之分。)

函数中 return 的单点也存在争议,尤其是在 MISRA-C 的上下文中(参见 )。然而,函数中的多个 returns 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;
}