简洁地退出函数的优雅方式,无需在C中使用goto
我们经常编写一些具有多个退出点的函数(即C中的简洁地退出函数的优雅方式,无需在C中使用goto,c,function,exit,goto,C,Function,Exit,Goto,我们经常编写一些具有多个退出点的函数(即C中的return)。同时,在退出函数时,对于一些常规工作(如资源清理),我们希望只实现一次,而不是在每个退出点实现它们。通常,我们可以通过如下方式使用goto来实现我们的愿望: void f() { ... ...{..{... if(exit_cond) goto f_exit; }..}.. ... f_exit: some general work such as cleanup } 我认为在这里使用got
return
)。同时,在退出函数时,对于一些常规工作(如资源清理),我们希望只实现一次,而不是在每个退出点实现它们。通常,我们可以通过如下方式使用goto来实现我们的愿望:
void f()
{
...
...{..{... if(exit_cond) goto f_exit; }..}..
...
f_exit:
some general work such as cleanup
}
我认为在这里使用goto是可以接受的,我知道很多人都同意在这里使用goto只是出于好奇,是否有任何优雅的方法可以在不使用C中的goto的情况下整齐地退出函数?例如
void f()
{
do
{
...
...{..{... if(exit_cond) break; }..}..
...
} while ( 0 );
some general work such as cleanup
}
或者您可以使用以下结构
while ( 1 )
{
//...
}
与使用goto语句相反,结构化方法的主要优点是它引入了编写代码的规程
我确信并且有足够的经验,如果一个函数有一个goto语句,那么经过一段时间,它将有几个goto语句。我想优雅可能意味着你很奇怪,你只是想避免使用goto
关键字,所以
您可以考虑使用和<代码> LojJMP < /C> >:
void foo() {
jmp_buf jb;
if (setjmp(jb) == 0) {
some_stuff();
//// etc...
if (bad_thing() {
longjmp(jb, 1);
}
};
};
我不知道它是否符合你的优雅标准。(我认为这不是很优雅,但这只是一种观点;然而,没有明确的goto
)
然而,有趣的是,longjmp
是一个非本地跳转:您可以(间接地)将jb
传递到一些东西
,并让其他一些例程(例如,由一些东西
调用)执行longjmp
。这可能会成为不可读的代码(因此请明智地对其进行注释)
甚至比longjmp
更丑陋:使用(在Linux上)
阅读和(以及方案中的操作)
当然,标准是一种优雅(且有用)的方法,可以实现某些功能。有时你也可以通过使用
顺便说一句,Linux内核代码经常使用goto
,包括一些被认为优雅的代码
我的观点是:IMHO不要狂热地反对
goto
-s,因为在某些情况下,使用(小心地)它实际上是优雅的。我已经看到了很多解决方案,它们在某种程度上往往是模糊的、不可读的和丑陋的
我个人认为最不丑陋的方式是:
int func (void)
{
if(some_error)
{
cleanup();
return result;
}
...
if(some_other_error)
{
cleanup();
return result;
}
...
cleanup();
return result;
}
是的,它使用两行代码而不是一行。所以它清晰、可读、可维护。这是一个完美的例子,说明了你必须用常识来对抗代码重复的下意识反射。清理函数只编写一次,所有清理代码都集中在那里 为什么要避免转到?
您要解决的问题是:如何确保在函数返回调用方之前始终执行一些公共代码?这是C程序员的问题,因为C不提供任何对RAII的内置支持
正如您在问题正文中已经承认的那样,goto
是一个完全可以接受的解决方案。尽管如此,避免使用它可能有非技术原因:
- 学术活动
- 编码标准符合性
- 个人心血来潮(我认为这是激发这个问题的原因)
goto
或break
),则可以将公共清理代码封装在函数中,并在早期返回时显式调用
int foo () {
...
if (SOME_ERROR) {
return foo_cleanup(SOME_ERROR_CODE, ...);
}
...
}
(这类似于另一个发布的答案,我只是在最初发布后才看到,但这里显示的表单可以利用兄弟调用优化。)
有些人觉得清晰更清晰,因此更优雅。其他人则认为需要将清除参数传递给函数,从而成为主要的诽谤者
添加另一层间接寻址。
在不更改用户API语义的情况下,将其实现更改为由两部分组成的包装器。第一部分执行函数的实际工作。第二部分在第一部分完成后执行必要的清理。如果每个部分都封装在自己的函数中,那么包装器函数有一个非常干净的实现
struct bar_stuff {...};
static int bar_work (struct bar_stuff *stuff) {
...
if (SOME_ERROR) return SOME_ERROR_CODE;
...
}
int bar () {
struct bar_stuff stuff = {};
int r = bar_work(&stuff);
return bar_cleanup(r, &stuff);
}
从执行工作的功能的角度来看,清理的“隐式”性质可能会被一些人看好。通过仅从单个位置调用cleanup函数,也可以避免一些潜在的代码膨胀。一些人认为“隐性”行为是“棘手的”,因此更难理解和维持
混杂的
可以考虑使用setjmp()
/longjmp()
的更深奥的解决方案,但正确使用它们可能会很困难。有一些开源包装器在其上实现了try/catch异常处理风格的宏(例如),但您必须更改编码风格才能使用该风格进行错误处理
也可以考虑像状态机那样实现函数。函数跟踪每个状态的进度,错误会导致函数对清除状态短路。这种风格通常只适用于特别复杂的函数,或者需要稍后重试并能够从中断处恢复的函数
入乡随俗。
如果您需要遵守编码标准,那么最好的方法是遵循现有代码库中最流行的任何技术。这几乎适用于对现有稳定源代码库进行更改的所有方面。引入一种新的编码风格会被认为是破坏性的。如果你觉得一个变化将是巨大的,你应该寻求当权者的批准
if(exit_cond) {
clean_the_mess();
return;
}
While ( exp ) {
for (exp; exp; exp) {
for (exp; exp; exp) {
if(exit_cond) {
clean_the_mess();
break;
}
}
}
}
void foo(exp)
{
if( ate_breakfast(exp)
&& tied_shoes(exp)
&& finished_homework(exp)
)
{
good_to_go(exp);
}
else
{
fix_the_problems(exp);
}
}