简洁地退出函数的优雅方式,无需在C中使用goto

简洁地退出函数的优雅方式,无需在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

我们经常编写一些具有多个退出点的函数(即C中的
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);
    }
}