错误处理中如何避免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 integer
或 bool
指示失败(而不是 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
语句的老鼠巢。循环和函数调用是更好的方法。
话虽如此,当谈到打破深层嵌套循环或错误处理时,goto
s 是一种合适的做事方式。 goto
清理模式在 linux 内核中广泛用于错误处理。所以对于 C 风格的编程,你所做的是完全可以接受的。
但是在 C++ 中通常有更好的选择。 RAII 和智能指针是不错的一等奖。例外应该涵盖 goto
的其余需求,尽管嵌入式系统通常禁用例外。
- 虽然我不使用
goto
,但在这种情况下,有些受人尊敬的程序员会使用 forward-gotos
。无论如何,还有一个替代解决方案:创建一个一次性循环。
- 我也同意其他人的看法,尽可能使用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
}
我知道带有 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 integer
或 bool
指示失败(而不是 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
语句的老鼠巢。循环和函数调用是更好的方法。
话虽如此,当谈到打破深层嵌套循环或错误处理时,goto
s 是一种合适的做事方式。 goto
清理模式在 linux 内核中广泛用于错误处理。所以对于 C 风格的编程,你所做的是完全可以接受的。
但是在 C++ 中通常有更好的选择。 RAII 和智能指针是不错的一等奖。例外应该涵盖 goto
的其余需求,尽管嵌入式系统通常禁用例外。
- 虽然我不使用
goto
,但在这种情况下,有些受人尊敬的程序员会使用forward-gotos
。无论如何,还有一个替代解决方案:创建一个一次性循环。 - 我也同意其他人的看法,尽可能使用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
}