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

rua*_*l93 30 c c++ goto c++11

我要说的是,goto在C/C++编程方面,使用被认为是一种不好的做法.

但是,给出以下代码

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

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

有没有其他被认为是好的做法?当我说使用goto被认为是一种不好的做法时,我错了吗?如果我,这是否是使用它的好情况之一?

谢谢

Max*_*hof 48

(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 */
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

  • 这是合理的,但肯定不是万能药。如果doLoopyStuff()依赖于calculateStuff中的20个变量,那将不会很好。如果`doMoreStuff()`想要对产生“命中”的循环索引做些事情,那也将很尴尬。 (2认同)

Ste*_*mit 24

如果你有这样的深层嵌套循环,你必须突破,我相信这goto是最好的解决方案.有些语言(不是C)有一个break(N)语句可以突破多个循环.C没有它的原因是它甚至比a 更糟糕goto:你必须计算嵌套循环来弄清楚它的作用,并且它很容易被后来的某个人出现并添加或删除嵌套级别,而没有注意到休息计数需要调整.

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

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

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

如果你坚持不使用a goto,并且你坚持不使用早期返回的单独函数,那么是的,你可以做一些事情,比如维护额外的布尔控制变量并在每个嵌套循环的控制条件下冗余地测试它们,但是这只是一种滋扰和混乱.(这是我所说的使用简单goto的小恶魔的一个更大的罪恶.)

  • *有些人说功能必须总是在最后返回,而不是从中间返回*我不喜欢那些人.为什么不在我们找到我们需要的东西后立即回来我总是问他们. (13认同)
  • 甚至[C++核心指南](https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#es76-avoid-goto)也说使用`goto`打破嵌套循环是好事! (8认同)
  • 是的!使用最好的工具来完成工作,即使它的名声不好。 (3认同)

ric*_*o19 15

我认为这goto是一个非常理智的事情,并且是C++核心指南中的特殊用例之一.

然而,或许另一个需要考虑的解决方案是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;
}();
Run Code Online (Sandbox Code Playgroud)

感谢JohnMcPineapple关于reddit的建议!


dbu*_*ush 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;
}
Run Code Online (Sandbox Code Playgroud)

要么:

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;
            ...
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

goto如果您只是向前跳(而不是向后),则使用a 被认为是可接受的,这样做可以使您的代码更具可读性和可理解性.

另一方面,如果您使用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);
}
Run Code Online (Sandbox Code Playgroud)

C++编译器会在这里抛出一个错误,因为goto跳转了初始化y.然而,C编译器将对此进行编译,并且上述代码将在尝试打印时调用未定义的行为,y如果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);
}
Run Code Online (Sandbox Code Playgroud)

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

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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • 值得注意的是,您的清理示例很容易通过惯用的C++实现.OP仍然没有决定他想要哪种语言. (3认同)
  • @Deduplicator我在这种情况下仅使用内存分配作为一个简单的例子.在更复杂的情况下,它可能涉及打开需要关闭的文件或套接字,从需要释放的库中获取资源,或者它们的任意组合以及分配内存. (2认同)

Yak*_*ont 5

Lambda 允许您创建本地范围:

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

如果您还希望能够返回该范围之外:

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;
}
Run Code Online (Sandbox Code Playgroud)

其中返回{}orboost::nullopt是一个“break”,并且返回一个值从封闭范围返回一个值。

另一种方法是:

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);
}
Run Code Online (Sandbox Code Playgroud)

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

中,这变成

for( auto[i,j,k] : cube( {0,N}, {0,N}, {0,N} ) ) {
}
Run Code Online (Sandbox Code Playgroud)

这很好。

现在,在需要响应的应用程序中,在主要控制流级别上循环较大的 3 维范围通常是一个坏主意。您可以将其关闭,但即使如此,您也会遇到线程运行时间过长的问题。我玩过的大多数 3 维大型迭代都可以从使用子任务线程本身中受益。

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

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

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

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

有时你只是使用 goto。或者您手动分解子循环的函数。或者您使用标志来打破深度递归。或者将整个 3 层循环放入其自己的函数中。或者您使用 monad 库编写循环运算符。或者您可以抛出异常 (!) 并捕获它。

中几乎每个问题的答案都是“视情况而定”。问题的范围和可用的技术数量很大,问题的细节会改变解决方案的细节。

  • 这就像一个案例研究,说明您可以使代码变得多么复杂,以避免简单的“goto”:) (2认同)
  • @LightnessRacesinOrbit *点头*是的,但在很多情况下你会得到其他好处。我已经从代码中删除了“goto”,因为我是 RAII 资源(从 C 风格的错误处理转向 C++ 风格的错误处理)。一旦我对一段代码进行了 lambda 化,我就可以去掉依赖项并重构为函数(缩短 100 行长函数以使它们更易于理解),或者允许将简单的 Nd 循环安全地转换为任务池基于一个(因为它被确定为性能瓶颈)。我不会仅仅为了删除 goto 而重写已工作的、经过测试的代码。但剥离它们可以简化控制流程。 (2认同)