C/C++:goto 进入 for 循环

13

我有一个不太寻常的情况 - 我想使用goto语句来跳转到循环内部,而不是跳出循环。

有很充分的理由这样做 - 这段代码必须是某个函数的一部分,该函数在第一次调用后进行一些计算,返回请求新数据并需要再次调用才能继续。函数指针(显然的解决方案)无法使用,因为我们需要与不支持函数指针的代码进行互操作。

我想知道下面的代码是否安全,即它将被所有符合标准的C/C++编译器正确编译(我们需要同时支持C和C++)。

function foo(int not_a_first_call, int *data_to_request, ...other parameters... )
{
    if( not_a_first_call )
        goto request_handler;
    for(i=0; i<n; i++)
    {
        *data_to_request = i;
        return;
request_handler:
        ...process data...
    }
}

我已经研究了一些标准,但是关于这种用法并没有太多的信息。我也想知道是否将 for 替换为等价的 while 是否从可移植性角度有益。

谢谢提前。

更新: 感谢所有评论者!

  1. 对于所有评论者 :) 是的,我明白我不能跳过局部变量的初始化器,并且每次调用都必须保存/恢复 i

  2. 关于强制理由 :) 这段代码必须实现反向通信接口。反向通信是一种编码模式,试图避免使用函数指针。有时必须使用它,因为旧代码期望您 使用它。

不幸的是,反向通信接口无法以良好的方式实现。您不能使用函数指针,也不能轻松地将工作拆分为几个函数。


9
"strong reasons to do so" -- 我在这篇文章中没有看到它们。 - Joe
1
你能不能将 request_handler 下面的代码放在一个单独的函数中,然后只需要调用该函数呢? - Tony The Lion
7
如果在标签之前声明仅限于for循环的局部变量,那么使用这些变量的表达式的行为是未定义的。实际上,我打赌对栈状态的任何假设都没有意义。显然,你在这里需要寻找协程,也许通过使用这个词进行谷歌搜索可以帮助你。 - Alexandre C.
12
必需:http://xkcd.com/292/这是一张由xkcd网站发布的漫画,标题为“Duty Calls”(责任呼唤)。漫画描述了一个人坐在电脑前面玩游戏的场景,突然电话响起,他回答电话后得知有一个重要的工作任务需要完成。尽管他不情愿接受任务,但他还是履行了自己的义务并完成了任务。漫画旨在表达执行责任的重要性。 - Xeo
1
我知道这是老问题了,但我只是好奇为什么你不直接调用一个函数(而不是像你说的函数指针那样)来完成相同的任务?也许我理解有误? - pqsk
显示剩余5条评论
7个回答

13

看起来完全合法。

从C99标准的草案http://std.dkuug.dk/JTC1/SC22/WG14/www/docs/n843.htm中,在跳转语句的章节中:

[#3] EXAMPLE 1 It is sometimes convenient to jump  into  the
   middle  of  a  complicated set of statements.  The following
   outline presents one possible approach to a problem based on
   these three assumptions:

     1.  The  general initialization code accesses objects only
         visible to the current function.

     2.  The  general  initialization  code  is  too  large  to
         warrant duplication.

     3.  The  code  to  determine  the next operation is at the
         head of the loop.  (To  allow  it  to  be  reached  by
         continue statements, for example.)

           /* ... */
           goto first_time;
           for (;;) {
                   // determine next operation
                   /* ... */
                   if (need to reinitialize) {
                           // reinitialize-only code
                           /* ... */
                   first_time:
                           // general initialization code
                           /* ... */
                           continue;
                   }
                   // handle other operations
                   /* ... */
           }

接下来,我们来看一下for循环语句:

[#1]  Except for the behavior of a continue statement in the |
   loop body, the statement

           for ( clause-1 ; expr-2 ; expr-3 ) statement

   and the sequence of statements

           {
                   clause-1 ;
                   while ( expr-2 ) {
                           statement
                           expr-3 ;
                   }
           }

结合您的问题,将这两者放在一起告诉您,您正跳过了某些步骤

i=0;

将代码插入到 while 循环的中间。这将执行

...process data...

然后

i++;

在流程控制跳转到while/for循环中的测试之前

i<n;

5

是的,这是合法的。

你所做的与Duff's Device等相比并不丑陋,而且也符合标准。

如@Alexandre所说,请勿使用goto跳过具有非平凡构造函数的变量声明。


我确定您不希望局部变量在调用之间被保留,因为自动变量生存期是如此基本。如果需要保留一些状态,则在C++中函数对象(functors)是一个很好的选择。C++0x lambda语法使它们更容易构建。在C中,您别无选择,只能通过指针由调用者传递来存储状态块。


它可能是合法的,但我仍然认为这是一件非常奇怪的事情。 - Tony The Lion
Duff的设备不使用goto - Sjoerd
3
Duff的设备使用了switch语句,它是一种goto形式。对Tony The Tiger说:我在初始问题中更新了一些这种解决方案背后的原因。 - Sergey
3
@Sergey,@Sjoerd:相关事实是Duff的设备也跳入了循环的中间。由于语法结构(switchcase标签)和循环交错在一起的方式,它看起来很丑陋。这也是它的美妙之处 :) - Ben Voigt

1

首先,我需要说的是,你必须重新考虑其他方式来完成这个任务。我很少看到有人使用goto,除非是用于错误管理

但如果你真的想坚持使用它,有几件事情你需要记住:

  • 从循环外跳转到中间不会使你的代码循环。(请查看下面的评论以获取更多信息)

  • 要小心,不要使用在标签之前设置的变量,例如引用*data_to_request。这包括在for语句中设置的i,当你跳转到标签时它没有被初始化。

就个人而言,我认为在这种情况下,我宁愿复制...处理数据的代码...,也不想使用goto。如果您仔细观察,您会注意到for循环内部的return语句,这意味着除非代码中存在跳转到标签的goto,否则标签的代码永远不会被执行。

function foo(int not_a_first_call, int *data_to_request, ...other parameters... )
{
    int i = 0;
    if( not_a_first_call )
    {
        ...process data...
        *data_to_request = i;
        return;
    }

    for (i=0; i<n; i++)
    {
        *data_to_request = i;
        return; 
    }
}

在这个特定的情况下,用户只在for循环中初始化了i,但并没有执行。很可能i将存储不满足循环条件的垃圾值。更新后的代码。 - karlphillip
1
由于循环前的代码似乎会在所有情况下将流量重定向到返回语句之后,因此返回语句不会造成问题。至于为什么要有一个不执行额外循环的for循环呢?嗯,它至少可以在潜在设置*data_to_request之前提供一个测试(i < n)。 - Jose_X
似乎重定向了流量。我通常不会基于假设工作。事实是... OP为我们编写了一段糟糕的代码,需要我们帮助他。 - karlphillip
我很少看到有人使用goto。Linux内核不仅用于错误管理,还用于清理和其他情况;https://www.kernel.org/doc/html/v4.17/process/coding-style.html中`goto`的原因列表包括“无条件语句更易于理解和遵循”以及“嵌套减少”。 - ShinTakezou

0

for循环的初始化部分将不会发生,这使得它有些多余。你需要在goto之前初始化i

int i = 0 ;
if( not_a_first_call )
    goto request_handler;
for( ; i<n; i++)
{
    *data_to_request = i;
    return;
request_handler:
    ...process data...
}

然而,这真的不是一个好主意

无论如何,代码都有缺陷,返回语句绕过了循环。按照现在的状态,它等同于:

int i = 0 ;

if( not_a_first_call )
    \\...process_data...

i++ ;
if( i < n )
{
    *data_to_request = i;
}

最后,如果你认为你需要这样做,那么你的设计就有缺陷,并且从你发布的片段来看,你的逻辑也有问题。

1
不,"..process_data.." 在循环内部。 - Jose_X
1
... process_data 部分之后是循环增量 i++ - Fred Foo
@Jose_X:是的,它曾经是循环,但不需要这样才能等效。走一遍代码;只有在not_a_first_call为真时才执行...process data...,并且由于goto的缘故,在处理for循环语句之前就已经执行了。然后进行i++i<n测试,分配*data_to_request,函数无条件返回,因此没有循环 - Clifford
@larsmans:发现得好(已编辑),但我想知道这是否真的很重要?这取决于i是否真正是该函数的外部变量,以及省略初始化是否是有意的,以及n的值。如果in确实是该函数的外部变量,那么这段代码存在的问题不仅仅是滥用了goto和for循环! - Clifford
这取决于 n 的值。是否“真的很重要”取决于 OP 是否真正需要这个(请原谅我的措辞)混乱代码块。 - Fred Foo

0

如果我理解正确,您正在尝试做一些类似于以下的事情:

  • 第一次调用foo时,它需要从其他地方请求一些数据,因此设置该请求并立即返回;
  • 在每次对foo的后续调用中,它会处理上一个请求的数据并设置新的请求;
  • 这将继续进行,直到foo处理完所有数据。

我不明白为什么在这种情况下您需要for循环;每次调用只迭代一次循环(如果我理解这里的用例)。除非i已声明为static,否则每次都会丢失其值。

为什么不定义一种类型来维护所有状态(例如i的当前值),然后定义一个围绕它的接口来设置/查询您需要的任何参数:

typedef ... FooState;

void foo(FooState *state, ...)
{
  if (FirstCall(state))
  {
    SetRequest(state, 1);
  }
  else if (!Done(state))
  {
    // process data;
    SetRequest(state, GetRequest(state) + 1);
  }
}

1
i的值并不一定会丢失,因为它很可能在其他地方被声明,可能是全局变量。此外,在“处理数据”部分之后执行i<n测试,在其中i可以相应地设置在i<n测试之前。从技术上讲,代码的每个部分都在某个时候被执行,并可能发挥有用的功能。 - Jose_X

0

不行,你不能这样做。我不知道这会产生什么效果,但我知道一旦你返回,你的调用堆栈就会被解开,变量i也就不存在了。

我建议重构代码。看起来你正在尝试构建一个类似于C#中的yield return的迭代器函数。也许你可以编写一个C++迭代器来实现这个功能?


2
我假设 i 是一个参数,因为没有本地定义,并且指示有其他参数。 - Ben Voigt
2
是的,我明白我必须恢复 i 的值。在实际代码中,它会随参数一起打包在特殊结构体中,并在每次调用时保存和恢复。 - Sergey

0

我觉得你没有声明i。从声明的位置完全取决于你所做的是否合法,但是请参见下面的初始化。

  • 在C语言中,您可以在循环之前或作为循环变量进行声明。但是,如果将其声明为循环变量,则在使用它时不会初始化其值,因此这是未定义的行为。如果在for之前声明它,则不会将0分配给它。
  • 在C++中,您无法跨越变量的构造函数,因此您必须在goto之前声明它。

在两种语言中,您都有一个更重要的问题,即i的值是否定义良好,以及如果该值有意义,则是否初始化。

如果有任何避免这种情况的方法,请不要这样做。或者,如果这确实非常关键,请检查汇编程序是否真正实现了您想要的功能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接