【问题标题】:Avoid repetition in C error handling避免 C 错误处理中的重复
【发布时间】:2018-02-16 13:29:20
【问题描述】:

我经常编写代码,这些代码最终是长序列之类的

int error;

error = do_something();
if (error) {
    return error;
}

error = do_something_else(with, some, args);
if (error) {
    return error;
}


error = do_something_yet_again();
if (error) {
    return error;
}

return 0;

我正在寻找一种更简洁的方法来编写它,从而在某种程度上避免重复的相同检查。到目前为止,我已经编写了一个 ERROR_OR 宏,其工作原理类似于

#define ERROR_OR(origerr, newerr)           \
    ({                                      \
        int __error_or_origerr = (origerr); \
        (__error_or_origerr != 0)           \
                ? __error_or_origerr        \
                : (newerr);                 \
    })

它允许原始代码变成类似的东西

int error = 0;

error = ERROR_OR(error, do_something());
error = ERROR_OR(error, do_something_else(with, some, args));
error = ERROR_OR(error, do_something_yet_again());

return error;

这(在我看来)更干净一些。它也不太容易理解,因为除非您阅读它的文档和/或实现,否则 ERROR_PRESERVE 宏的功能并不明显。它也没有解决重复的问题,只是更容易将所有(现在是隐式的)检查写在一行上。

我真的很想重写这一切,如下所示:

return ERROR_SHORT_CIRCUIT(
    do_something(),
    do_something_else(with, some, args),
    do_something_yet_again()
);

假设的ERROR_SHORT_CIRCUIT 宏会

  • 在其参数列表中获取可变数量的表达式
  • 按顺序计算每个表达式
  • 如果每个表达式的计算结果都为零,则其本身的计算结果为零
  • 如果任何表达式的计算结果为非零,则立即终止并计算为最后一个表达式的值

最后一个条件是我的短路与直接使用 || 运算符不同的地方——因为这将评估为 1 而不是错误值。

我最初写这篇文章的尝试如下:

#define ERROR_SHORT_CIRCUIT(firsterr, ...)          \
    ({                                              \
        int __error_ss_firsterr = (firsterr);       \
        (__error_ss_firsterr != ERROR_NONE)         \
                ? __error_ss_firsterr               \
                : ERROR_SHORT_CIRCUIT(__VA_ARGS__); \
    })

这有两个明显的问题:

  • 它不处理其基本情况(当__VA_ARGS__ 是单个值时)
  • C 不支持递归宏

我研究了一些recursive macro hacks,但我不喜欢使用那种程度的预处理器魔法——太多的空间让某些事情发生微妙的错误。我也考虑过使用真实(可能是可变参数)函数,但这需要

  • 放弃短路行为
  • 将函数作为指针传递,从而规范化它们的签名

而且这两个似乎都比原始的显式代码更糟糕。

我很想听听有关处理此问题的最佳方法的建议。我对许多不同的方法持开放态度,但我的最终目标是在不影响可读性的情况下避免重复。

(我想很明显,我对 || 运算符在 Ruby 等语言中的行为感到有些羡慕)。

【问题讨论】:

  • 我不认为这更好,只是可读性较差。此外,error-or 的行为与您首先显示的代码完全不同。不要太看中宏;许多项目的错误处理代码比其他任何东西都多。如果你的就是其中之一,那就这样吧。专注于可维护和可读的代码。 (旁观:({ … }) 是 gcc 扩展。
  • 我感觉这个问题将被关闭,因为它主要是基于意见的。但是,您可以通过将每个函数调用块放在一行中来使代码更紧凑,例如,if ((error = do_something())) return error; if ((error = do_something_else(with, some, args))) return error; if ((error = do_something_yet_again())) return error;
  • 顺便说一句:if ( error ) return error; 是少数几个我不使用大括号并且可能单行的情况之一。
  • 就我个人而言,我喜欢将宏用于样板错误处理代码,有点像您的ERROR_OR()。这种做法使程序逻辑在错误处理中比没有它们时更清楚地突出。我不担心尽量减少宏扩展产生的错误处理代码的数量。
  • @JohnBollinger:问题是第一个 sn-p 做的事情与第二个非常不同。将宏内的代码流更改到外部会很快让您遇到维护问题..

标签: c gcc error-handling c-preprocessor


【解决方案1】:

我会使用如下代码:

if ((error = do_something()) != 0 ||
    (error = do_something_else(with, some, args)) != 0 ||
    (error = do_something_yet_again()) != 0)
    return error;
return 0;

它是完全定义的,因为在每个 || 运算符之前都有序列点。它真的不需要宏。它仅在您在函数调用之间分配资源或执行其他操作时遇到问题,但这与您的示例代码显示的不同。至少 90% 的战斗是创建 do_something_or_other() 函数序列,以便轻松处理错误序列。

【讨论】:

  • 删除!= 0 部分怎么样?风格问题?
  • 样式问题。我非常不喜欢 if ((error = do_something())) 以避免 GCC 抱怨在条件中使用赋值;我宁愿使用!= 0 表示法。
  • 我猜这与上面的ERROR_OR 并没有太大的不同——两者都有明确的分配,但这使用了标准运算符,这是一个改进。感谢您的建议!
【解决方案2】:

另一种选择:

int error = 0;
do {
    // Note: extra parens suppress assignment-as-conditional warning

    if ((error = do_something())) break;
    if ((error = do_something_else())) break;
    if ((error = do_yet_another_thing())) break;
    error = do_a_final_thing();
} while (0);
return error;

【讨论】:

  • 我承认,我从来都不喜欢用do { ... } while (0); 代替 goto,在这种情况下,我认为我更喜欢显式检查。不过,感谢您的建议!
猜你喜欢
  • 1970-01-01
  • 2017-02-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-01-29
  • 2013-01-13
  • 1970-01-01
相关资源
最近更新 更多