防止不断检查错误的模式?

3
在C语言中,是否有一种模式可以消除在调用其他函数的函数中不断检查错误的需要?
例如,如果函数foo()依次调用a()、b()和c(),则必须在继续之前检查每个函数的返回值。如果a()、b()或c()还调用其他函数,可能还会调用其他函数,这将留下一个可能是相同错误的长序列的错误检查...
int16_t a(x_t x) {return DO_SOME_WORK(x);}

int16_t b(y_t y) {return DO_OTHER_WORK(y);}

int16_t c(z_t z) {return DO_MORE_WORk(z);}

foo (x_t x, y_t y, z_t z) {
    int16_t err = 0;
    // I can handle errors returned by either:
    err = a(x));
    if (err) return err;
    err = b(y);
    if (err) return err;
    err = c(z); 
    return err;

    // Or
    err = a(x);
    if (!err) err = b(y);
    if (!err) err = c(z);
    return err;
}

我倾向于第二种方法,因为它有一个明确的单一退出点,但是另一种方法本地化了处理,并且这两种方法都引入了很多if语句。对于大型程序来说,这意味着很多额外的代码,特别是在那些只是调用其他函数并在它们之间传递输出的函数中。 谢谢。

3
我认为另外一个真正的方法是通过setjmplongjmp模拟异常,但这样会使你的程序流程变得更加难以跟踪。此外,一种常见的编译器优化是将所有的return err语句合并为一个,并插入有条件的分支到该语句。 - Drew McGowen
err 值是 0 和 1 还是有许多不同的 err 值?根据答案,使用不同的习语。 - chux - Reinstate Monica
1
@chux 在许多不同的错误值中,它们最终被返回到上层函数时,它们会被推送到错误队列以报告给父系统。 - Toby
3个回答

4
在这种情况下,您可以利用短路评估的&&,其中当左手操作数为false时,不会评估右手操作数。
如果您想在调用a()b()c()之间执行其他处理,则可以使用以下方式:
bool foo (x_t x, y_t y, z_t z) 
{
    bool err = a( x );
    err = err && b( y ) ;
    err = err && c( z ) ; 

    return err;
}

如果函数只由调用a()b()c()组成,那么你可以使用更加简洁的方法:

bool foo (x_t x, y_t y, z_t z) 
{
    return a( x ) && 
           b( y ) && 
           c( z ) ; 
}

[回复评论时添加]

解决@kkrambo提出的函数返回任何数字值不会传递给调用方的问题是可行的,但作为解决方案越来越不具有吸引力:

int16_t foo (x_t x, y_t y, z_t z) 
{
    int16_t ret ;

    bool err = (ret = a( x ));
    err = err && (ret = b( y )) ;
    err = err && (ret = c( z )) ; 

    return ret ;
}

或者

int16_t foo (x_t x, y_t y, z_t z) 
{
    uint16_t ret ;

    (ret = a( x )) && 
    (ret = b( y )) && 
    (ret = c( z )) ;

    return ret ; 
}

你甚至可以做到更糟糕:

int16_t foo (x_t x, y_t y, z_t z) 
{
    int16_t err ;

    if( (err = a(x)) ) {}
    else if( (err = b(y)) ) {} 
    else if( (err = c(z)) ) {}

    return err ; 
}

以下是简化后的内容,其中err为布尔值:
bool foo (x_t x, y_t y, z_t z) 
{
    bool err = true ;

    if( a(x) ) {}
    else if( b(y) ) {} 
    else if( c(z) ) {}
    else { err = false }

    return err ; 
}

从技术上讲,你“可以”这样做,但下一个维护你代码的人会恨你。 :) - Andrew Cottrell
3
在原问题中,这些函数在成功时返回0或可以标识各种错误情况的非零值。但对于您的建议,这些函数必须在成功时返回true,对于所有可能的错误则返回false。任何描述性错误代码都将丢失。 - kkrambo
@AndrewCottrell:代码的可取性是一个不同的问题。我认为从那个角度来看,第一种模式比第二种更可取,但会重复评估"err"。简洁和“聪明”并不总是更好。 - Clifford
1
@kkrambo:没错,但在这个例子中,return 仅被用作 foo() 函数内的布尔值。虽然任何 err 的数值都被传递并返回给 foo() 的调用者,但这段代码可能不等同且不能满足所有要求。 - Clifford

3
如果一个函数的每个调用都需要错误检查,那么每次调用该函数时都应该检查错误。原因是即使是相同的函数输入也不同,全局(和静态局部)变量也不同,会改变结果。这意味着如果您想要检查错误,实际上并没有浪费计算工作。
编辑:我不知道这是否明智,但您可以使用宏来清理语法。
err = a(x);
if(err) return err;

它看起来会像这样:

它将呈现如下

#define error_check(f, e) \
e = f; \
if(e) return e;

因此,在您的代码中,您只需编写以下内容:
error_check(a(x),err)

我对宏不太熟悉,但这应该是可行的。


这个想法非常棒:“对于每个函数调用,参数或全局变量都可以有不同的值;因此,如果您想检查错误,您不会浪费工作。” 真的,非常好。 - linuxfan says Reinstate Monica

1

由于目标是返回错误代码而不仅仅是布尔值,因此 OP 提供的两种方法至少是合理的,如果不是更可取的。像 if (a(x) || b(x)) return true 这样的短路代码未能保持从 a()b() 派生的 err 值。

这归结为风格。

如果代码只包含几行,任何一种方法都可以。但是考虑到可能存在在 a(x)b(x)c(x) 之间交织着大量代码,建议不要使用风格2,因为它将流程分散到许多代码行中。我喜欢风格3胜过1,因为它更加简洁,但是1和3之间的差异很小。

为了应对风格#2收集错误返回的能力,考虑一个更高级别的函数包装器,确保所有序言和尾声活动都发生。

最佳选择:遵循团队的风格指南,否则选择能够维护的最佳方案。

int16_t foo (x_t x, y_t y, z_t z) {  // added matching return type
    int16_t err = 0;

    // OP style 1
    err = a(x));
    if (err) return err;
    err = b(y);
    if (err) return err;
    err = c(z); 
    return err;

    // OP style 2
    err = a(x);
    if (!err) err = b(y);
    if (!err) err = c(z);
    return err;

    // style 3
    // via enum or define, create an OK symbol
    if ((err = a(x)) != OK) return err;
    if ((err = b(x)) != OK) return err;
    if ((err = c(x)) != OK) return err;
    // 
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接