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

时间:2014-09-10 13:37:10

标签: c function exit goto

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

void f()
{
    ...
    ...{..{... if(exit_cond) goto f_exit; }..}..
    ...
    f_exit:
    some general work such as cleanup
}

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

7 个答案:

答案 0 :(得分:31)

为什么要避免goto

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

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

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

对猫进行皮肤处理总有不止一种方法,但优雅作为标准过于主观,无法提供缩小到单一最佳替代品的方法。你必须自己决定最好的选择。

明确调用清理函数

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

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);
}

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

其它...

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

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

像罗马人那样做。

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

答案 1 :(得分:6)

例如

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

    some general work such as cleanup
}

或者您可以使用以下结构

while ( 1 )
{
   //...
}

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

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

答案 2 :(得分:5)

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

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

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

  ...

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

  ...

  cleanup();
  return result;
}

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

答案 3 :(得分:5)

我认为这个问题非常有趣,但不受主观性的影响是无法回答的,因为优雅是主观的。我的想法如下:一般来说,你想要在你描述的场景中做的是阻止控制沿着执行路径传递一系列语句。其他语言会这样做提出一个你必须抓住的例外情况。

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

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

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

答案 4 :(得分:4)

我想优雅可能意味着你怪异并且你只是想避开goto关键字,所以...... < / SUP>

您可以考虑使用setjmp(3)longjmp

void foo() {
jmp_buf jb;
if (setjmp(jb) == 0) {
   some_stuff();
   //// etc...
   if (bad_thing() {
       longjmp(jb, 1);
   }
 };
};

我不知道它是否符合你的优雅标准。 (我认为它不是很优雅,但这只是一个意见;但是,没有明确的goto)。

然而,有趣的是,longjmp非本地跳转:您可以(间接)jb传递给some_stuff并拥有一些其他例程(例如some_stuff调用)执行longjmp。这可能会成为不可读的代码(因此明智地评论)。

甚至比longjmp更丑:使用(在Linux上)setcontext(3)

了解continuationsexceptions(以及Scheme中的call/cc操作)。

当然,标准exit(3)是一种优雅(有用)的方法,可以解决某些功能问题。你有时也可以使用atexit(3)

来玩巧妙的技巧 BTW,Linux内核代码经常使用goto,包括在一些被认为是优雅的代码中。

我的观点是:恕我直言不要对goto - s 狂热,因为有些情况下使用(小心)它实际上很优雅。

答案 5 :(得分:3)

我是粉丝:

void foo(exp) 
{
    if(    ate_breakfast(exp)
        && tied_shoes(exp)
        && finished_homework(exp)
      )
    {
        good_to_go(exp);
    }
    else
    {
        fix_the_problems(exp);
    }
}

其中ate_breakfast,tied_shoes和finished_homework接受指向他们工作的exp的指针,并返回指示该特定测试失败的bool。

有助于记住short circuit evaluation在这里起作用 - 这可能有点像某些人的代码气味,但就像其他人一直在说的那样,优雅有点主观。

答案 6 :(得分:-1)

goto语句永远不是必需的,而且在不使用它的情况下编写代码也更容易。

相反,您可以使用更清洁的功能并返回。

if(exit_cond) {
    clean_the_mess();
    return;
}

或者你可以像Vlad上面提到的那样打破。但是有一个缺点,如果你的循环有深层嵌套的结构,那么它只会从最里面的循环中退出。 例如:

While ( exp ) {
    for (exp; exp; exp) {
        for (exp; exp; exp) {
            if(exit_cond) {
                clean_the_mess();
                break;
            }
        }
    }
}

只会退出内部for循环,并且不会放弃进程。