如何避免使用goto并有效地破坏嵌套循环

时间:2018-05-25 13:15:21

标签: c++ c c++11 goto

我说在使用C / C ++进行编程时,使用goto被认为是一种不好的做法。

但是,给出以下代码

for (i = 0; i < N; ++i) 
{
    for (j = 0; j < N; j++) 
    {
        for (k = 0; k < N; ++k) 
        {
            ...
            if (condition)
                goto out;
            ...
        }
    }
}
out:
    ...

我想知道如何有效地实现相同的行为而不使用goto。我的意思是我们可以做一些事情,例如在每个循环结束时检查condition,但是AFAIK goto将只生成一个汇编指令,它将是jmp。所以这是我能想到的最有效的方法。

还有其他被认为是好习惯吗?当我说使用goto被认为是一种不好的做法时,我错了吗?如果是的话,这是否会成为使用它的好方法之一?

谢谢

14 个答案:

答案 0 :(得分:47)

(imo)最好的非goto版本看起来像这样:

void calculateStuff()
{
    // Please use better names than this.
    doSomeStuff();
    doLoopyStuff();
    doMoreStuff();
}

void doLoopyStuff()
{
    for (i = 0; i < N; ++i) 
    {
        for (j = 0; j < N; j++) 
        {
            for (k = 0; k < N; ++k) 
            {
                /* do something */

                if (/*condition*/)
                    return; // Intuitive control flow without goto

                /* do something */
            }
        }
    }
}

拆分它也可能是一个好主意,因为它可以帮助你保持功能简短,代码可读(如果你比我更好地命名函数)和依赖性低。

答案 1 :(得分:23)

如果你有类似深度嵌套的循环而你必须爆发,我相信goto是最好的解决方案。有些语言(不是C)有一个break(N)语句,可以突破多个循环。 C没有它的原因是它甚至更糟而不是goto:你必须计算嵌套循环来弄清楚它的作用,并且它很容易被后来有人带来并添加或删除嵌套级别,而不会注意到需要调整中断计数。

是的,gotos通常不赞成。在这里使用goto不是一个好的解决方案;它只是几个邪恶中最少的。

在大多数情况下,您必须打破深度嵌套循环的原因是因为您正在搜索某些内容,并且您已找到它。在那种情况下(以及其他一些评论和答案已经提出),我更喜欢将嵌套循环移动到它自己的函数中。在这种情况下,内循环中的return可以非常干净地完成您的任务。

(有些人说功能必须总是在最后返回,而不是从中间返回。那些人会说简单的突破性功能解决方案因此无效,他们会即使搜索被分解为自己的功能,也强制使用相同的笨拙的内循环技术。个人而言,我认为那些人错了,但你的里程可能会有所不同。)

如果你坚持不使用goto,如果你坚持不使用早期返回的单独函数,那么是的,你可以做一些事情,比如维护额外的布尔控制变量并在控制每个嵌套循环的条件,但这只是一个麻烦和混乱。 (这是我用简单的goto所说的更大的罪恶之一,而不是。)

答案 2 :(得分:15)

我认为goto在这里是完全理智的事情,并且是C++ Core Guidelines的特殊用例之一。

然而,或许另一个需要考虑的解决方案是IIFE lambda。在我看来,这比声明一个单独的函数稍微优雅一点!

[&] {
    for (int i = 0; i < N; ++i)
        for (int j = 0; j < N; j++)
            for (int k = 0; k < N; ++k)
                if (condition)
                    return;
}();

感谢reddit上的JohnMcPineapple提出此建议!

答案 3 :(得分:12)

在这种情况下,你要避免使用goto

一般情况下应避免使用goto,但此规则有例外情况,您的情况就是其中之一的一个很好的例子。

让我们来看看替代方案:

for (i = 0; i < N; ++i) {
    for (j = 0; j < N; j++) {
        for (k = 0; k < N; ++k) {
            ...
            if (condition)
                break;
            ...
        }
        if (condition)
            break;
    }
    if (condition)
        break;
}

或者:

int flag = 0
for (i = 0; (i < N) && !flag; ++i) {
    for (j = 0; (j < N) && !flag; j++) {
        for (k = 0; (k < N) && !flag; ++k) {
            ...
            if (condition) {
                flag = 1
                break;
            ...
        }
    }
}

这些都不像goto版本那样简洁或可读。

在您只是向前跳(而不是向后)的情况下,使用goto被认为是可以接受的,这样做可以使您的代码更具可读性和可理解性。

另一方面,如果您使用goto向两个方向跳转,或者跳转到可能会绕过变量初始化的范围,那么 会很糟糕。

以下是goto的一个不好的例子:

int x;
scanf("%d", &x);
if (x==4) goto bad_jump;

{
    int y=9;

// jumping here skips the initialization of y
bad_jump:

    printf("y=%d\n", y);
}

C ++编译器会在此处抛出错误,因为goto会跳过y的初始化。然而,C编译器将对此进行编译,上述代码将在尝试打印y时调用undefined behavior,如果发生goto,则goto将被取消初始化。

正确使用void f() { char *p1 = malloc(10); if (!p1) { goto end1; } char *p2 = malloc(10); if (!p2) { goto end2; } char *p3 = malloc(10); if (!p3) { goto end3; } // do something with p1, p2, and p3 end3: free(p3); end2: free(p2); end1: free(p1); } 的另一个例子是错误处理:

void f()
{
    char *p1 = malloc(10);
    if (!p1) {
        return;
    }
    char *p2 = malloc(10);
    if (!p2) {
        free(p1);
        return;
    }
    char *p3 = malloc(10);
    if (!p3) {
        free(p2);
        free(p1);
        return;
    }
    // do something with p1, p2, and p3

    free(p3);
    free(p2);
    free(p1);
}

这将在函数结束时执行所有清理操作。将此与替代方案进行比较:

Option Explicit

Sub SortColumnsIndividually()
    Dim Column As Range
    For Each Column In Selection.Columns
        ActiveSheet.Sort.SortFields.Clear
        ActiveSheet.Sort.SortFields.Add Key:=Column, _
            SortOn:=xlSortOnValues, Order:=xlAscending, DataOption:=xlSortNormal
        With ActiveSheet.Sort
            .SetRange Column
            .Header = xlNo
            .MatchCase = False
            .Orientation = xlTopToBottom
            .SortMethod = xlPinYin
            .Apply
        End With
    Next Column
End Sub

清理在多个地方完成。如果您以后添加了需要清理的更多资源,您必须记住在所有这些地方添加清理以及清除之前获得的任何资源。

上面的例子与C比C ++更相关,因为在后一种情况下你可以使用具有适当析构函数和智能指针的类来避免手动清理。

答案 4 :(得分:4)

替代方案 - 1

您可以执行以下操作:

  1. 在开头bool
  2. 中设置isOkay = true变量
  3. 您的所有for循环条件,添加额外条件isOkay == true
  4. 当您的自定义条件满足/失败时,请设置isOkay = false
  5. 这将使你的循环停止。额外的bool变量有时会很方便。

    bool isOkay = true;
    for (int i = 0; isOkay && i < N; ++i)
    {
        for (int j = 0; isOkay && j < N; j++)
        {
            for (int k = 0; isOkay && k < N; ++k)
            {
                // some code
                if (/*your condition*/)
                     isOkay = false;
            }
         }
    }
    

    替代方案 - 2

    其次。如果上面的循环迭代在一个函数中,那么当满足自定义条件时,最好选择return结果。

    bool loop_fun(/* pass the array and other arguments */)
    {
        for (int i = 0; i < N ; ++i)
        {
            for (int j = 0; j < N ; j++)
            {
                for (int k = 0; k < N ; ++k)
                {
                    // some code
                    if (/* your condition*/)
                        return false;
                }
            }
        }
        return true;
    }
    

答案 5 :(得分:4)

Lambdas允许您创建本地范围:

[&]{
  for (i = 0; i < N; ++i) 
  {
    for (j = 0; j < N; j++) 
    {
      for (k = 0; k < N; ++k) 
      {
        ...
        if (condition)
          return;
        ...
      }
    }
  }
}();

如果您还希望返回的能力超出该范围:

if (auto r = [&]()->boost::optional<RetType>{
  for (i = 0; i < N; ++i) 
  {
    for (j = 0; j < N; j++) 
    {
      for (k = 0; k < N; ++k) 
      {
        ...
        if (condition)
          return {};
        ...
      }
    }
  }
}()) {
  return *r;
}

返回{}boost::nullopt是&#34; break&#34;,并返回一个值会从封闭范围返回一个值。

另一种方法是:

for( auto idx : cube( {0,N}, {0,N}, {0,N} ) {
  auto i = std::get<0>(idx);
  auto j = std::get<1>(idx);
  auto k = std::get<2>(idx);
}

我们在所有3个维度上生成一个迭代,并使其成为一个深度嵌套循环。现在break工作正常。你必须写cube

中,这就变成了

for( auto[i,j,k] : cube( {0,N}, {0,N}, {0,N} ) ) {
}

这很好。

现在,在您应该响应的应用程序中,在原始控制流级别上循环大的3维范围通常是个坏主意。您可以将其断开,但即使这样,您最终也会遇到线程运行太长的问题。我玩过的大多数三维大型迭代都可以从使用子任务线程中受益。

为此,您最终希望根据其访问的数据类型对您的操作进行分类,然后将您的操作传递给为您安排迭代

auto work = do_per_voxel( volume,
  [&]( auto&& voxel ) {
    // do work on the voxel
    if (condition)
      return Worker::abort;
    else
      return Worker::success;
  }
);

然后涉及的控制流程进入do_per_voxel函数。

do_per_voxel不是一个简单的裸循环,而是一个系统,用于将每个体素任务重写为每个扫描线任务(甚至每个平面任务,具体取决于平面的大小) / scanlines在运行时(!))然后将它们依次发送到线程池托管任务调度程序,缝合生成的任务句柄,并返回可以等待或用作延续的类似未来的work工作完成时触发。

有时你只是使用goto。或者您手动分解子循环的功能。或者你使用标志来突破深度递归。或者你将整个3层循环放在它自己的函数中。或者使用monad库组合循环运算符。或者你可以抛出异常(!)并抓住它。

中几乎每个问题的答案是&#34;它取决于&#34;。问题的范围和您可用的技术数量很大,问题的细节会改变解决方案的细节。

答案 6 :(得分:3)

将你的for循环分解为函数。 它使事情变得更容易理解,因为现在你可以看到每个循环实际上在做什么。

bool doHerpDerp() {
    for (i = 0; i < N; ++i) 
    {
        if (!doDerp())
            return false;
    }
    return true;
}

bool doDerp() {
    for (int i=0; i<X; ++i) {
        if (!doHerp())
            return false;
    }
    return true;
}

bool doHerp() {
    if (shouldSkip)
        return false;
    return true;
}

答案 7 :(得分:3)

  

还有其他被认为是好习惯吗?我错了   我说使用goto被认为是一种不好的做法?

goto可能被误用和过度使用,但我在你的例子中没有看到任何两个。一个简单的goto label_out_of_the_loop;最清楚地表达了一个深度嵌套的循环。

使用跳转到不同标签的许多goto是不好的做法,但在这种情况下,关键字goto本身不会使您的代码变坏。事实上,你在代码中跳来跳去,这很难让人难以理解。但是,如果你需要单独跳出嵌套循环,那么为什么不使用为此目的而制作的工具。

用薄薄的空气比喻:想象一下,你生活在一个过去髋关节将钉子钉入墙壁的世界里。最近,使用螺丝刀将螺丝拧入墙壁变得更加时髦,而且锤子完全不合时宜。现在考虑你必须(尽管有点老 - fashinoned)钉在墙上。你不应该不使用锤子来做那件事,但也许你应该问自己,你是否真的需要在墙上钉钉而不是螺钉。

(以防它不清楚:锤子是goto并且墙上的钉子是从嵌套环中跳出来的,而墙上的螺钉将使用功能来完全避免深度嵌套; )

答案 8 :(得分:2)

就您在Visual Studio 2017上发布模式中编辑两个选项的效率评论而言,产生完全相同的程序集。

for (int i = 0; i < 5; ++i)
{
    for (int j = 0; j < 5; ++j)
    {
        for (int k = 0; k < 5; ++k)
        {
            if (i == 1 && j == 2 && k == 3) {
                goto end;

            }
        }
    }
}
end:;

并带有旗帜。

bool done = false;
for (int i = 0; i < 5; ++i)
{
    for (int j = 0; j < 5; ++j)
    {
        for (int k = 0; k < 5; ++k)
        {
            if (i == 1 && j == 2 && k == 3) {
                done = true;
                break;
            }
        }
        if (done) break;
    }
    if (done) break;
}

都产生..

xor         edx,edx  
xor         ecx,ecx  
xor         eax,eax  
cmp         edx,1  
jne         main+15h (0C11015h)  
cmp         ecx,2  
jne         main+15h (0C11015h)  
cmp         eax,3  
je          main+27h (0C11027h)  
inc         eax  
cmp         eax,5  
jl          main+6h (0C11006h)  
inc         ecx  
cmp         ecx,5  
jl          main+4h (0C11004h)  
inc         edx  
cmp         edx,5  
jl          main+2h (0C11002h)    

所以没有收获。如果您使用现代c ++编译器将另一个选项包装在lambda中。

[](){
for (int i = 0; i < 5; ++i)
{
    for (int j = 0; j < 5; ++j)
    {
        for (int k = 0; k < 5; ++k)
        {
            if (i == 1 && j == 2 && k == 3) {
                return;
            }
        }

    }
}
}();

再次产生完全相同的组件。我个人认为在你的例子中使用goto是完全可以接受的。很清楚其他人正在发生什么,并使代码更简洁。可以说lambda同样简洁。

答案 9 :(得分:2)

的具体

IMO,在这个具体的例子中,我认为注意循环之间的通用功能很重要。 (现在我知道你的例子不一定是文字的,但只是忍受我一段时间)因为每个循环迭代N次,你可以重构你的代码,如下所示:

实施例

int max_iterations = N * N * N;
for (int i = 0; i < max_iterations; i++)
{
    /* do stuff, like the following for example */
    *(some_ptr + i) = 0; // as opposed to *(some_3D_ptr + i*X + j*Y + Z) = 0;
    // some_arr[i] = 0; // as oppose to some_3D_arr[i][j][k] = 0;
}

现在,重要的是要记住所有循环,而对于if-goto范例,它实际上只是合成糖。我同意其他人你应该有一个函数返回结果,但是我想展示一个像上面的例子,其中可能不是这种情况。当然,我在代码审查中对上述内容进行了标记,但如果您将上面的内容替换为goto,我会认为这是朝着错误方向迈出的一步。 (注意 - 确保您可以可靠地将其装入所需的数据类型)

一般

现在,作为一般答案,每次循环的退出条件可能不一样(如相关帖子)。作为一般规则,尽可能多地从循环(乘法等)中拉出尽可能多的不需要的操作,而编译器每天变得越来越聪明,没有替代编写高效且可读的代码。

实施例

/* matrix_length: int of m*n (row-major order) */
int num_squared = num * num;
for (int i = 0; i < matrix_length; i++)
{
    some_matrix[i] *= num_squared; // some_matrix is a pointer to an array of ints of size matrix_length
}

而不是编写*= num * num,我们不再需要依赖编译器来为我们优化(尽管任何好的编译器都应该)。因此,执行上述功能的任何双重或三重嵌套循环不仅会使您的代码受益,而且IMO会为您编写干净,高效的代码。在第一个例子中,我们可以改为*(some_3D_ptr + i*X + j*Y + Z) = 0;!我们是否相信编译器可以优化i*Xj*Y,以便每次迭代都不会计算它们?

bool check_threshold(int *some_matrix, int max_value)
{
    for (int i = 0; i < rows; i++)
    {
        int i_row = i*cols; // no longer computed inside j loop unnecessarily.
        for (int j = 0; j < cols; j++)
        {
            if (some_matrix[i_row + j] > max_value) return true;
        }
    }
    return false;
}

呸!为什么我们不使用STL提供的类或像Boost这样的库? (我们必须在这里做一些低级别/高性能的代码)。由于复杂性,我甚至无法编写3D版本。即使我们手动优化了某些东西,如果您的编译器允许,使用#pragma unroll或类似的预处理器提示甚至可能更好。

结论

通常,您可以使用的抽象级别越高越好,但是如果将一维的1维行整数顺序矩阵混叠到2维数组会使您的代码流更难理解/扩展,那么这值得?同样,这也可能是使某事成为其自身功能的指标。我希望,根据这些例子,你可以看到在不同的地方需要不同的范例,而你作为程序员的工作可以解决这个问题。不要为上述事情发疯,但请确保您知道它们的含义,如何使用它们以及何时需要它们,最重要的是,确保使用您的代码库的其他人知道它们是什么对此没有任何疑虑。祝你好运!

答案 10 :(得分:1)

一种可能的方法是为表示状态的变量分配一个布尔值。稍后可以使用“IF”条件语句测试此状态,以便稍后在代码中用于其他目的。

答案 11 :(得分:1)

bool meetCondition = false;
for (i = 0; i < N && !meetCondition; ++i) 
{
    for (j = 0; j < N && !meetCondition; j++) 
    {
        for (k = 0; k < N && !meetCondition; ++k) 
        {
            ...
            if (condition)
                meetCondition = true;
            ...
        }
    }
}

答案 12 :(得分:1)

已经有几个很好的答案告诉你如何重构代码,所以我不再重复了。不再需要以这种方式编码以提高效率;问题是它是否不优雅。 (好吧,我会建议一个改进:如果你的帮助函数只打算在那个函数体内使用,你可以通过声明它们static来帮助优化器,所以它肯定知道该函数没有外部链接,永远不会从任何其他模块调用,并且提示inline不会受到伤害。但是,之前的答案说,当你使用lambda时,现代编译器不需要任何这样的提示。)

我将稍微挑战问题的框架。你是对的,大多数程序员都禁止使用goto。在我看来,这已经忽视了最初的目的。当Edsger Dijkstra写道,“Go To Statement Considered Harmful”时,他有这样一个特定的理由:go to的“肆无忌惮”使用使得正式推理当前的程序状态变得困难,以及必须具备哪些条件与来自递归函数调用(他更喜欢的)或迭代循环(他接受的)的控制流相比,目前是正确的。他总结道:

  

目前的go to陈述太过原始;这是一个太多的邀请,使一个人的程序混乱。人们可以看到并理解被视为对其使用的条款。我并不认为所提到的条款是详尽无遗的,因为它们将满足所有需求,但无论提出何种条款(例如堕胎条款),它们都应满足程序员独立坐标系统的要求,以便描述有用和可管理的方式。

许多类C语言编程语言(例如Rust和Java)确实有一个额外的“条款被视为对其使用的限制”,break标签。更加有限的语法可能类似于break 2 continue;来突破嵌套循环的两个级别,并在包含它们的循环顶部继续。对于Dijkstra想要做的事情而言,这不仅仅是C风格break的问题:定义程序员可以在头脑中跟踪的程序状态的简明描述,或静态分析器可以找到易处理的。 / p>

goto限制为这样的结构使其成为标签的重命名中断。剩下的问题是编译器和程序员不一定知道你只会这样使用它。

如果在循环之后存在重要的后置条件,并且您对goto的关注与Dijkstra相同,您可以考虑在简短的评论中说明它,例如// We have done foo to every element, or encountered condition and stopped.减轻人类的问题,静态分析仪应该可以。

答案 13 :(得分:0)

最好的解决方案是将循环放在一个函数中,然后从该函数中放入return

这与您的goto示例基本相同,但是大量利益可以避免再次发生goto辩论。

简化的伪代码:

bool function (void)
{
  bool result = something;


  for (i = 0; i < N; ++i) 
    for (j = 0; j < N; j++) 
      for (k = 0; k < N; ++k) 
        if (condition)
          return something_else;
  ...
  return result;
}

此处的另一个好处是,如果遇到两种以上的情况,您可以从bool升级到enum。您无法以可读的方式使用goto真正做到这一点。您开始使用多个gotos和多个标签的那一刻,就是您接受意大利面条编码的那一刻。是的,即使你只是向下分支 - 阅读和维护也不是很好。

值得注意的是,如果您有3个嵌套for循环,那么这可能表明您应该尝试将代码分解为多个函数,然后整个讨论可能甚至都不相关。