错误处理中如何避免goto

How avoid goto in error processing

我知道带有 goto 的程序是多么可怕和难以阅读,我想像其他人一样不惜一切代价避免使用它们。

然而,我遇到了一个情况,我在拥有一个优雅的程序的同时,也很难避开它们。它带有错误处理。我做了很多步骤,如果任何一步有错误,程序应该显示错误信息,释放一些内存和 return false。如果一切正常,它应该 return true.

原程序是这样的:

bool init_routine()
{
     int errcode;   // Error code
     errcode = do_someting();
     if(errcode != RETURN_SUCCESS)
     {
         free_some_memory();
         std::cerr << "Something went wrong in initialization.";
         return false;
     }
     errcode = do_something_else();
     if(errcode != RETURN_SUCCESS)
     {
         free_some_more_memory();   // Was allocated in the do_something function
         free_some_memory();
         std::cerr << "Something went wrong in initialization.";
         return false;
     }
     /* The same pattern repeats quite a few times... */
     return true;   // Everything was ok
}

这很丑陋,因为有些代码是一遍又一遍地复制粘贴,这是不好的做法。我可以使用函数,但是函数不能从被调用者 return false,因为 C++ 缺少我所说的 double return 指令 .

另一种方法是使用嵌套 if :

bool init_routine()
{
     int errcode;   // Error code
     errcode = do_someting();
     if(errcode == RETURN_SUCCESS)
     {
         errcode = do_something_else();
         if(errcode == RETURN_SUCCESS)
         {
             /* The same pattern repeats quite a few times */
             if()
             {
                if()
                {
                      if()
                      {
                         return true;
                      }
                }
             }
         }
         free_some_more_memory();
     }
     free_some_memory();
     std::cerr << "Something went wrong in initialization.";
     return false;
}

这避免了任何重复并且紧凑。然而,随着嵌套 if 的数量快速增长,缩进成为阻碍。而且,所有这些 if 隐藏了程序的简单控制流程,它只是按顺序执行指令并在错误时退出,并使 reader 相信有很多复杂的条件可以遵循。

我认为更自然的另一种方式是:

bool init_routine()
{
     int errcode;   // Error code
     errcode = do_someting();
     if(errcode != RETURN_SUCCESS)
     {
         goto err1;
     }
     errcode = do_something_else();
     if(errcode != RETURN_SUCCESS)
     {
         goto err2;
     }
     /* The same pattern repeats quite a few times... */
     return true;   // Everything was ok

err2:
     free_some_more_memory();
err1:
     free_some_memory();
     std::cerr << "Something went wrong in initialization";
     return false;
}

没有重复,唯一的问题是使用了邪恶的goto。我的问题是:

有没有办法不使用 goto,同时不使用重复,不使用异常,并且没有疯狂的缩进?

编辑:为了清楚起见,我不能使用异常,因为我调用的例程来自用 C 编写的外部库,并且我从我的 C++ 代码中调用它们。很抱歉没有早点提到这一点。

如果绝对必须显式分配,请使用标准集合或 std::unique_ptr 等智能指针。他们自动进行重新分配。就是这样。

更一般地说,使用带有清除析构函数的对象(标准集合和智能指针就是这样做的)。这就是所谓的 RAII,资源获取即初始化。 Google 要了解更多信息,请在课本索引中查找。

顺便说一句,将输出混合到通用函数中并不是一个好主意。如果您想在 GUI 程序中使用该代码怎么办?

如果您使用代表所有权的对象(例如使用智能指针或类似的东西)来管理分配的资源,这些问题就不会发生:

resource r;
if (int e = alloc_resource(&r))
    return e;

r 的析构函数将负责在作用域退出时释放资源。

虽然使用错误代码而不是异常是完全愚蠢的,因为它们占据了返回值位置,只携带有限数量的信息,你可能会忘记检查它们。使用例外。

不问为什么尽量避免异常:

有几种错误处理方法。首先是 C 风格的方式。 Return integerbool 指示失败(而不是 void)或 return integer 上的错误代码(在 C 中通常是全局变量 errno 用于此)。还有丑的goto.

更多C++风格的方式:

如果您的函数 returns 对象或值,您可以定义 无效的 对象(例如带有 width = height = 0 的图像)。

您提到的 c++ 中缺少 «double return» 在 c++11 中不存在。你可以这样做:return std::make_pair(obj,true).

如果您需要在错误情况下处理资源,我给您以下建议:不要这样做。请改用 RAII。省心又省心

标准方法是使用 unique_ptr 或类似方法。如果这对你来说太不方便并且你使用的是 c++0x 或更高版本,你可以使用 finally lambdas:

T* resource = some_legacy_c_function();
if(!resource) return false; //or throw, or whatever
FINALLY([&](){
    cleanup(resource);
});
do_stuff_with(resource);
return;//cleanup is called automatically here :)

finally 宏定义如下(将其放在 header 的某处):

template<typename F>
class _Finally
{
private:
    F _f;

public:
    inline _Finally(const F& f) :
        _f(f)
    {
    }

    inline ~_Finally(){
        _f();
    };
};

template<typename F>
inline _Finally<F> Finally(const F& f)
{
    return _Finally<F>(f);
}

#define FINALLY auto __finally_##__LINE__ = Finally

goto 通常被禁止,因为这通常会导致没有结构的 if-then-goto 语句的老鼠巢。循环和函数调用是更好的方法。

话虽如此,当谈到打破深层嵌套循环或错误处理时,gotos 是一种合适的做事方式。 goto 清理模式在 linux 内核中广泛用于错误处理。所以对于 C 风格的编程,你所做的是完全可以接受的。

但是在 C++ 中通常有更好的选择。 RAII 和智能指针是不错的一等奖。例外应该涵盖 goto 的其余需求,尽管嵌入式系统通常禁用例外。

  1. 虽然我不使用 goto,但在这种情况下,有些受人尊敬的程序员会使用 forward-gotos。无论如何,还有一个替代解决方案:创建一个一次性循环。
  2. 我也同意其他人的看法,尽可能使用RAII。但是在某些情况下(例如遗留 do_something())您可以使用布尔标志做一些 'manual-RAII'。

所以你的代码可以这样重构:

bool init_routine()
{
    int errcode;   // Error code
    bool need_free_some_memory = false;
    bool need_free_some_more_memory = false;

    do { //not a loop, just scope for break

        need_free_some_memory = true;
        errcode = do_something();

        if(errcode != RETURN_SUCCESS)
            break;

        need_free_some_more_memory = true;
        errcode = do_something_else();
        if(errcode != RETURN_SUCCESS)
            break;

    } while(0);

    if(need_free_some_more_memory)
        free_some_more_memory();   // Was allocated in the do_something function
    if(need_free_some_memory)
        free_some_memory();

    if(errcode != RETURN_SUCCESS)
        std::cerr << "Something went wrong in initialization.";

    return errcode == RETURN_SUCCESS;   // Everything was ok
}