无需在C中使用goto即可完美退出功能的优雅方式

ACc*_*tor 24 c goto function exit

我们经常编写一些具有多个出口点的函数(即return在C中).同时,当退出函数时,对于一些常规工作,例如资源清理,我们希望只实现一次,而不是在每个出口点实现它们.通常,我们可以通过使用如下所示的goto来实现我们的愿望:

void f()
{
    ...
    ...{..{... if(exit_cond) goto f_exit; }..}..
    ...
    f_exit:
    some general work such as cleanup
}
Run Code Online (Sandbox Code Playgroud)

我认为在这里使用goto是可以接受的,我知道很多人同意在这里使用goto.出于好奇,是否有任何优雅的方式可以在C中不使用goto而整齐地退出函数?

jxh*_*jxh 31

为何避免goto

您要解决的问题是:如何确保在函数返回调用方之前总是执行一些公共代码?对于C程序员来说这是一个问题,因为C没有为RAII提供任何内置支持.

正如您已在问题体中承认的那样,这goto是一个完全可以接受的解决方案.从来没有,可能有非技术原因,以避免使用它:

  • 学术练习
  • 编码标准合规性
  • 个人心血来潮(我认为这是激发这个问题的原因)

总是有不止一种方法给猫皮肤,但优雅作为一个标准太主观,无法提供一种缩小到单一最佳选择的方法.你必须自己决定最好的选择.

显式调用清理函数

如果避免显式跳转(例如,gotobreak),可以将常用清理代码封装在函数中,并在早期明确调用return.

int foo () {
    ...
    if (SOME_ERROR) {
        return foo_cleanup(SOME_ERROR_CODE, ...);
    }
    ...
}
Run Code Online (Sandbox Code Playgroud)

(这类似于另一个已发布的答案,我在最初发布后才看到,但此处显示的表单可以利用兄弟调用优化.)

有些人觉得外显更清晰,因而更优雅.其他人认为需要将清理参数传递给函数以成为主要的detractor.

添加另一层间接.

在不更改用户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);
}
Run Code Online (Sandbox Code Playgroud)

从执行工作的功能的角度来看,清理的"隐含"性质可能会被一些人看好.仅通过从单个位置调用清理函数也可以避免一些潜在的代码膨胀.一些人认为"隐性"行为是"棘手的",因此更难以理解和维护.

杂...

使用setjmp()/ longjmp()可以考虑更多深奥的解决方案,但正确使用它们可能很困难.有一些开源包装器可以在它们上面实现try/catch异常处理样式宏(例如cexcept),但是你必须改变你的编码风格以使用该样式进行错误处理.

还可以考虑像状态机一样实现该功能.该功能跟踪每个状态的进度,错误导致功能短路到清理状态.这种样式通常保留用于特别复杂的功能,或者需要稍后重试并且能够从中断的位置拾取的功能.

做到入乡随俗.

如果您需要遵守编码标准,那么最好的方法是遵循现有代码库中最常用的技术.这适用于对现有稳定源代码库进行更改的几乎所有方面.引入新的编码风格将被视为具有破坏性.如果您认为更改会显着改善软件的某些方面,您应该寻求权力的批准.否则,因为"优雅"是主观的,为了"优雅"而争论不会让你到处都是.


Vla*_*cow 6

例如

void f()
{
    do
    {
         ...
         ...{..{... if(exit_cond) break; }..}..
         ...
    }  while ( 0 );

    some general work such as cleanup
}
Run Code Online (Sandbox Code Playgroud)

或者您可以使用以下结构

while ( 1 )
{
   //...
}
Run Code Online (Sandbox Code Playgroud)

与使用goto语句相反的结构方法的主要优点是它引入了编写代码的规则.

我确信并且有足够的经验,如果一个函数有一个goto语句,那么通过一段时间它会有几个goto语句.:)

  • 任何其他名称的转到仍然是非条件分支.与goto示例相比,这没有增加任何内容.它在任何方面都不是更好. (14认同)
  • @VladfromMoscow你的代码如何防止这种情况?让我们进化一下:`做{if(something)continue; if(exit_cond)中断; if(something_else);/*什么都不做*/if(this_or_that)休息; 否则if(the_sun_shines){do_stuff(); 如果(某事)破裂; }继续; 做东西(); } while(0);`等等.仅仅因为你的代码不包含goto关键字并不意味着它对意大利面很安全. (10认同)
  • 结构体?我只看到控制流操作员滥用. (3认同)

Lun*_*din 5

我已经看到很多解决方案如何做到这一点,并且在某种程度上它们往往是模糊,难以理解和丑陋的.

我个人认为最不丑陋的方式是:

int func (void)
{
  if(some_error)
  {
    cleanup();
    return result;
  }

  ...

  if(some_other_error)
  {
    cleanup();
    return result;
  }

  ...

  cleanup();
  return result;
}
Run Code Online (Sandbox Code Playgroud)

是的,它使用两行代码而不是一行代码.所以?它清晰,可读,可维护.这是一个完美的例子,你必须在反对代码重复和使用常识的情况下对抗你的反身反应.清理功能只写一次,所有清理代码都集中在那里.

  • 在当前正在运行的OpenSSL评审/重写中,这种代码被认为是非常危险的.这是一个公开的邀请,可以犯错误并搞乱代码. (2认同)

mid*_*dor 5

我认为这个问题非常有趣,但是如果不受主观性的影响就无法回答,因为优雅是主观的.我对它的看法如下:一般来说,你想要在你描述的场景中做的是阻止控制沿着执行路径传递一系列语句.其他语言可以通过引发异常来实现此目的,您必须抓住它.

我已经写下了整齐的黑客来做你想要做的事情几乎所有C语言中的控制语句,有时是组合,但我认为它们只是表达跳到特殊点的想法的非常模糊的方式.相反,我只是指出我们如何达到goto可能更可取地步:再一次,你想要表达的是发生了一些阻止遵循常规执行路径的事情.某些东西不仅仅是一个常规条件,可以通过在路径上采用不同的分支来处理,但有些东西使得无法在当前状态下以安全的方式使用常规返回点的路径.我认为在这一点上有三个选择:

  1. 通过条件条款返回
  2. 转到错误标签
  3. 每个可能失败的语句都在条件语句中,而常规执行被认为是一系列条件操作.

如果你的清理在每个可能的紧急出口都足够相似,我宁愿使用goto,因为冗余地编写代码会使函数混乱.我认为你应该交换返回点的数量和你创建的复制清理代码,以防止使用goto的尴尬.两种解决方案都应该被接受为程序员的个人选择,除非有严重的理由不这样做,例如,您同意所有功能必须只有一个退出.但是,任何一种的使用都应该是代码中的结果和一致的.第三种选择是 - imo - goto中可读性较差的表兄弟,因为最终你会跳到一组清理例程 - 可能也包含在else语句中,但是这使得人们更难以遵循常规由于条件语句的深度嵌套,程序流程.

tl; dr:我认为在条件返回和基于后续风格决策的goto之间进行选择是最优雅的方式,因为它是表达代码背后的最具表现力的方式,清晰度是优雅的.