在c++中执行多个相关错误检查的优雅方式,不需要goto,也不需要提前返回

Elegant way to perform multiple dependent error checks in C++ without goto, and without early return?

本文关键字:不需要 方式 goto 返回 执行 c++ 检查 错误      更新时间:2023-10-16

假设我想连续调用四个函数,它们对我的某个对象进行操作。如果它们中的任何一个失败,我想返回FAILURE而不调用其他的,如果它们都成功完成,我想返回SUCCESS

通常,我会这样做:

if(function_zero(&myMutableObject) == SUCCESS)
{
    return FAILURE;
}
if(function_one(&myMutableObject) == SUCCESS)
{
    return FAILURE;
}
if(function_two(&myMutableObject) == SUCCESS)
{
    return FAILURE;
}
if(function_three(&myMutableObject) == SUCCESS)
{
    return FAILURE;
}
return SUCCESS;

或者,如果我需要做一些清理:

if(function_zero(&myMutableObject) == SUCCESS)
{
    status = FAILURE;
    goto cleanup;
}
if(function_one(&myMutableObject) == SUCCESS)
{
    status = FAILURE;
    goto cleanup;
}
if(function_two(&myMutableObject) == SUCCESS)
{
    status = FAILURE;
    goto cleanup;
}
if(function_three(&myMutableObject) == SUCCESS)
{
    status = FAILURE;
    goto cleanup;
}
cleanup:
// necessary cleanup here
return status;

然而,我正在做的项目有一些限制:

  • No goto, ever
  • 不提前返回(每个函数一次返回)
  • 线长限制
  • (EDIT)无例外
  • (EDIT)没有模板。

这使我得出这样的结论:

if(function_zero(&myMutableObject) == SUCCESS)
{
    if(function_one(&myMutableObject) == SUCCESS)
    {
        if(function_two(&myMutableObject) == SUCCESS)
        {
            status = function_three(&myMutableObject);
        }
        else
        {
            status = FAILURE;
        }
    }
    else
    {
        status = FAILURE;
    }
}
else
{
    status = FAILURE;
}
return status;

不幸的是,这经常使我超出行长限制。

我的问题是:有没有更简单的方法来写这个?

指出,限制:

    我必须在这里隐含的代码块中实现这个逻辑。我不能创建新的功能,重构或改变整体架构。
  • (编辑)在现实中,函数有非常不同的签名。

使用异常和RAII。这些都是它们被发明出来要解决的问题。然而,异常更多的是一个系统范围的特性,而不是你可以在本地应用的东西。

对于清理块,RAII正是您所需要的特性。

对于成功/失败,我们可以使用lambdas和variadics将它们隐式地链接在一起。

现在我们可以简单地将它们写成列表中的lambda。

status f() {
    struct nested {
        static template<typename F> status_t Check(F f) {
            return f();
        }
        static template<typename F, typename... Chain> status_t Check(F f, Chain... chain) {
            auto status = f();
            return status != failure ? Check(chain...) : status;
        }
    };
    return nested::Check( 
        [] { return function_zero(&myMutableObject); },
        [] { return function_one(&myMutableObject); },
        [] { return function_two(&myMutableObject); },
        [] { return function_three(&myMutableObject); },
    );
}
如果您需要捕获返回值,

这会变得稍微更有问题,但是由于它似乎总是带有参数的错误代码,如果您只是在f()中声明接收变量应该是好的,那么所有未来的lambda都可以引用它。它也不要求每个函数都有相同的签名,或者分配不同的数据结构。

仅在当前使用&&操作符。如果设置为FAILURE,则&&将立即失败,并且不会执行后续测试。

status = SUCCESS
if (status == SUCCESS && function_zero(&myMutableObject) == FAILURE)
{
    status = FAILURE;
}
if (status == SUCCESS && function_one(&myMutableObject) == FAILURE)
{
    status = FAILURE;
}
if (status == SUCCESS && function_two(&myMutableObject) == FAILURE)
{
    status = FAILURE;
}
if (status == SUCCESS && function_three(&myMutableObject) == FAILURE)
{
    status = FAILURE;
}
return status;

正如@Mooing Duck建议的那样,您可以简单地在else if链中完成所有操作:

status = SUCCESS
if (function_zero(&myMutableObject) == FAILURE)
{
    status = FAILURE;
}
else if (function_one(&myMutableObject) == FAILURE)
{
    status = FAILURE;
}
else if (function_two(&myMutableObject) == FAILURE)
{
    status = FAILURE;
}
else if (function_three(&myMutableObject) == FAILURE)
{
    status = FAILURE;
}
return status;

我在处理长链C调用(通常是winapi)时使用过几次的模式是这样的:

bool ret =
    function_zero(&myMutableObject) == SUCCESS
    &&
    function_one(&myMutableObject) == SUCCESS
    &&
    function_two(&myMutableObject) == SUCCESS
    &&
    function_three(&myMutableObject) == SUCCESS;
if(!ret)
{
    // cleanup
}
return ret?SUCCESS:FAILURE;

您甚至可以在每行的末尾留下&&,这样它看起来更像一个"正常"的调用序列(尽管我个人更喜欢这样,它更清楚发生了什么)。

&&操作符保证以正确的顺序执行,并且只有在前面的调用成功的情况下才执行,并在调用之间引入必要的序列点(或者在c++ 11中如何调用它们),因此在各种调用之间计算参数的顺序是很好的定义的。而且,它的优先级足够低,不需要额外的括号。

如果您不害怕使用com风格的宏,您还可以将== SUCCESS检查封装在宏中,如

// in some header, maybe with a less abused name
#define OK(x) ((x) == SUCCESS)
bool ret =
    OK(function_zero(&myMutableObject))
    &&
    OK(function_one(&myMutableObject))
    &&
    OK(function_two(&myMutableObject))
    &&
    OK(function_three(&myMutableObject));
// ...

更好的是,如果SUCCESS != 0FAILURE == 0,你可以完全放弃OK()== SUCCESS,只使用&&来连接调用。

你说没有例外,但我认为你应该知道你放弃了什么。

如果您使用基于RAII的清理并将错误报告为异常而不是代码,那么您将得到如下代码:

function_zero(&myMutableObject);
function_one(&myMutableObject);
function_two(&myMutableObject);
function_three(&myMutableObject);

这是一个解释正确的c++异常处理及其好处的站点:

http://exceptionsafecode.com/

这些好处包括:

  • 更容易阅读
    • 更容易理解和维护
    • 代码错误
  • 更容易编写
  • 在成功路径上改进了性能
      <
    • "零成本"异常/gh>
    • 编译器将异常理解为一种语言特性,知道哪条路径是成功路径,哪条路径是失败路径
    • 异常表的代码大小增加通过消除错误检查代码
    • 来抵消

此外,Herb Sutter对"单次退出"(你的"不提前返回"规则的通用名称)有这样的看法:

一般来说,请注意SE/SE是一个过时的想法,并且一直都是错了。"单条目",或者函数应该总是这样在一个地方进入(在他们的开始),而不是用goto从跳跃调用者的代码直接到函数体内的随机位置,是这是计算机科学中非常有价值的进步。这就是使库成为可能,因为它意味着你可以打包一个函数并重用它,函数总是知道它的起始状态,它开始的地方,不管调用代码是什么。"单一出口,"另一方面,在优化的基础上得到了不公平的普及如果有一个单一的返回,编译器可以执行返回值优化更好"(见上面的反例)和对称性("如果单一入口是好的,单一出口也必须是好的’),但这是错误的因为这些原因不能反向保持——允许调用者插入是坏的,因为它不在函数的控制之下,但允许函数本身在知道它已经完成的时候提前返回是完美的精细,完全在功能控制之下。

http://herbsutter.com/category/c/gotw/page/4/

您应该尝试更新规则,即使这只可能用于新项目。

可以将函数指针存储在std::function的容器中(只要它们具有与示例中相同的签名):

std::vector<std::function<error_code (myobject&)> functions { func1, func2, func3, func4 };
error_code status = SUCCESS;
for (const auto& f : functions) {
    if (f(myobject) == ERROR) {
        clean_up();
        status = ERROR;
        break;
    }
}
return status;

这可以工作:

bool success = true;
success = success && function_zero(&myMutableObject) != FAILURE;
success = success && function_one(&myMutableObject) != FAILURE;
success = success && function_two(&myMutableObject) != FAILURE;
success = success && function_three(&myMutableObject) != FAILURE;
return success ? SUCCESS : FAILURE;

return可以替换为:

int status = SUCCESS;
if( !success ) status = FAILURE;
return status;

if在您的公司也被禁止使用条件运算符。

你可以写一个简单的局部函子:

bool fn0(int&) { return false; }
bool fn1(int&) { return false; }
bool fn2(int&) { return false; }
int main() {
    struct Test {
        const bool result;
        typedef bool (*function)(int&);
        Test(function f, int& value)
        :   result(f(value))
        {};
        operator bool () const { return result; }
    };
    int value;
    return Test(fn0, value)
        && Test(fn1, value)
        && Test(fn2, value);
}