解开Knuth的结:如何重组意大利面条代码?

时间:2016-05-06 18:39:57

标签: c++ algorithm loops refactoring software-design

这个问题的灵感来自于How to transform a flow chart into an implementation?,它询问了从算法中消除代码中goto语句的方法。 answer科学论文中描述了this一般问题。

我已经在Knuth的算法X的高级草图之后实现了一些代码计算机编程的艺术描述 Lexicographic的生成具有受限前缀的排列(参见本draft第16页)。

这是上述算法的相应flow chart

这可能是非常聪明且非常高效的算法,但代码的结构似乎很难遵循。我最终使用了良好的旧goto式实现:

//Algorithm X;
1:
initialize();
2:
enter_level(k);
3:
set(a[k],q);
if(test() == ok) {
  if (k == n) {
    visit();
    goto 6;
  }
  goto 4;
}
goto 5;
4:
increase(k);
goto 2;
5:
increasev2(a[k]);
if (q != 0) {
  goto 3;
}
6:
decrease(k);
if (k==0) {
  goto 7;
}
set(p,u_k);
goto 5;
7:
return;

问题是:如何重构此代码以消除所有 goto次来电?

一个(虚假的)答案是建议"查阅引用的科学论文,并逐行跟踪它" - 实际上,这当然是一种可能性。但是这个问题是关于经验丰富的程序员在看到这个spaghetti code之后立即看到的内容。

我对如何重构一步一步感兴趣,而不仅仅是代码。

注意:

  1. 基于其高级规范和goto跳转实际实现算法X是直截了当的。实现黑盒函数initialize()等只需要一些额外的指令,但这些指令与代码的结构无关。在函数调用期间发生的事情并不重要,因为现在的重点是程序的流程。
  2. "的常见辩论是GOTO still considered harmful?"与此问题完全无关,并且在答案和评论中 根本不应该被解决。
  3. 相关:how to work with or complete the spaghetti code?

5 个答案:

答案 0 :(得分:3)

没有太多的努力(而且风险很大),你可以快速减少一些东西和标签。

1)删除未在任何地方引用的标签(这将是标签1:)

2)查找除goto之外无法输入的代码块,这些代码块在少数地方被调用。这些通常可以简单地考虑在内。 4:可以通过将代码移动到它所调用的位置来处理,并且安全地完成,因为它的唯一退出是goto。这也允许我们删除它上面的goto 5,因为该代码将简单地落到5:。 7:可以通过修改if语句来处理。此时我们已经

initialize();
2:
enter_level(k);
3:
set(a[k],q);
if(test() == ok) {
  if (k == n) {
    visit();
    goto 6;
  }
  increase(k);
  goto 2;
}
5:
increasev2(a[k]);
if (q != 0) {
  goto 3;
}
6:
decrease(k);
if (k!=0) {
  set(p,u_k);
  goto 5;
}
return;

我倾向于止步于此。但是,如果你继续,它就成了识别循环并用循环结构替换gotos的问题。但是,由于代码的结构方式,进行这些更改的风险似乎要大得多。此外,你可能最终会休息并继续进行,无论如何都是一种类型。我最终得到的是(如果没有一些非常严格的测试,我不会保证其正确性):

initialize();
enter_level(k);
while (true) {
  set(a[k],q);
  if(test() == ok) {
    if (k == n) {
      visit();
    } else {
      increase(k);
      enter_level(k);
      continue;
    }
  } else {
    increasev2(a[k]);
    if (q != 0) {
      continue; 
    }
  }
  while (true) {
    decrease(k);
    if (k!=0) {
      set(p,u_k);
      increasev2(a[k]);
      if (q != 0) {
        break; 
      }
    } else {
      return;
    }
  }
}

我做了3:循环,6:内循环。我通过复制5:代码代替goto,并用休息替换goto 3来摆脱goto 5。这使得制作更清洁的循环变得容易一些。 goto 6通过使用else来修复。 goto 3将继续。

在此之后(如果剩下能量)你可以尝试从while(true)改变循环,并继续进入具有实际条件的while。

首先开发测试然后进行一两次更改并进行测试是一个好主意。进行另一次更改,然后再次测试。如果你不这样做,很容易在早期发生结构性错误,然后使随后的步骤无效并迫使你重新开始。

答案 1 :(得分:2)

在c ++中,算法可以写成:

void initialize() {}
void enter_level(int k) {}
void set(int x,int y) {}
bool test() { return true; }
void visit() {}
void increase(int k) {}
void increasev2(int k) {}
void decrease(int k) {}

void algorithm_x()
{
    int k{0};
    int a[] ={1,2,3,4,5};
    int q{0};
    bool ok{true};
    int n{0};
    int p{0};
    int u_k{0};

        //Algorithm X;
    lbl1:
        initialize();
    lbl2:
        enter_level(k);
    lbl3:
        set(a[k],q);
        if (test() == ok) {
            if (k == n) {
                visit();
                goto lbl6;
            }
            goto lbl4;
        }
        goto lbl5;
    lbl4:
        increase(k);
        goto lbl2;
    lbl5:
        increasev2(a[k]);
        if (q != 0) {
            goto lbl3;
        }
    lbl6:
        decrease(k);
        if (k==0) {
            goto lbl7;
        }
        set(p,u_k);
        goto lbl5;
    lbl7:
        return;

}

int main()
{
    algorithm_x();
    return 0;
}

假设我们不使用break语句,那么程序可以是:

void initialize() {}
void enter_level(int k) {}
void set(int x,int y) {}
bool test() { return true; }
void visit() {}
void increase(int k) {}
void increasev2(int k) {}
void decrease(int k) {}

void algorithm_x()
{
    int k{0};
    int a[] ={1,2,3,4,5};
    int q{0};
    bool ok{true};
    int n{0};
    int p{0};
    int u_k{0};

    bool skiptail{false};

    //Algorithm X;
    initialize();
    enter_level(k);
    while (true) {

        skiptail = false;
        set(a[k],q);
        if (test() == ok) {
            if (k == n) {
                visit();
                decrease(k);
                if (k==0) {
                    return;
                }
                set(p,u_k);
                while (true) {
                    increasev2(a[k]);
                    if (q != 0) {
                        //goto lbl3;
                        skiptail = true;
                    }
                    if (!skiptail) decrease(k);
                    if (!skiptail) if (k==0) {
                        return;
                    }
                    if (!skiptail) set(p,u_k);
                }
            }
            if (!skiptail) increase(k);
            if (!skiptail) enter_level(k);
            //goto lbl3;
            skiptail = true;
        }
        if (!skiptail) while (true) {
            increasev2(a[k]);
            if (q != 0) {
                //goto lbl3;
                skiptail = true;
            }
            if (!skiptail) decrease(k);
            if (!skiptail) if (k==0) {
                return;
            }
            if (!skiptail) set(p,u_k);
        }
        if (!skiptail) increase(k);
        if (!skiptail) enter_level(k);
        //goto lbl3;
        skiptail = true;
        if (!skiptail) while (true) {
            increasev2(a[k]);
            if (q != 0) {
                //goto lbl3;
                skiptail = true;
            }
            if (!skiptail) decrease(k);
            if (!skiptail) if (k==0) {
                return;
            }
            if (!skiptail) set(p,u_k);
        }
    }

}

int main()
{
    algorithm_x();
    return 0;
}

更改使用以下算法:

  1. 摆脱未使用的标签。删除lbl1

  2. 如果标签以goto结尾,则将该块替换为使用它的任何位置。 移除lbl4lbl6lbl7

  3. 如果标签返回自身,则将块放入while(true)。 移除底部lbl5lbl5现已自包含,可在使用时替换)

  4. 如果块是自包含的,则替换它使用的任何位置。 删除lbl5

  5. 如果一个标签跟随另一个标签,则在块的末尾放置一个转到下一个标签,以便可以按规则2替换它。 移除lbl2(可以goto lbl3

  6. 现在,我们在整个代码中留下了最后一个标签goto。将goto lbl3替换为skiptail=true,将剩余的块放在while (true)块中,并设置剩余的语句以检查是否skiptail=false。 移除lbl3并替换为skiptail = false

答案 2 :(得分:2)

我从未使用goto,但这似乎是一个有趣的挑战,所以我亲自尝试重构。

首先,浏览代码,查看每个标签goto的语句数量;重要的是要记住避免错误。在你的例子中,没有任何东西导致1,所以我们可以忽略它。

有时,我发现在控制流暗示它们时添加goto是很有用的。当我在代码之间移动代码时,它有助于跟踪顺序。

重构goto的最佳方法是从内到内向上工作。

  • 最后一条指令是7:return;,可以简单地将其移动到调用goto 7的位置。这很容易。

  • 接下来,我尝试查看哪些标签以goto(无条件)结尾,并直接来自不同的goto。在这种情况下,这将是4;它可以在2前面移动,在一个由哨兵控制的内部(准备一个循环)。 (goto我的第一点是看到现在可以删除2。)

  • 我接下来要做的就是将5和6放入循环中。如果我错了,我无论如何都可以回溯。

  • 此时,我看到6将在3或5之后执行。我也看到5可以执行3,所以我决定在5之后移动3.我添加一个变量以便我可以跳过5第一次。我在6月底将它设置为true。

  • 为了确保5可以在需要时直接转到6,我可以用5执行的相反条件在if语句中包含3。当我确实需要从5点到3点时,我可以在5点内改变条件,这样就可以直接执行3点。

  • 此时我只有一个goto,从3变为4.如果我将其更改为break,我可以退出一个循环,然后到达结尾。为了得到4,我只是将所有东西(除了1)包裹在一个循环中。

如果您正在使用goto,则可以使用this trick来突破嵌套循环,但在这种情况下,这不是必需的。

最后,我得到了这段代码(仅为清晰起见而包含标签):


1: initialize();
reached4=false;
do5 = false;
while(true){
    if (reached4){
      4: increase(k);
    }
    2: enter_level(k);
    while(true){
      if(do5){
        5:
        increasev2(a[k]);
        if (q != 0) {
          do5 = false;//goto 3
        }
      }
      if(!do5){
        3:
        set(a[k],q);
        if(test() == ok) {
          if (k == n) {
            visit();//goto 6;
          }else{
            reached4 = true;
            break;//goto 4
          }
        }
      }
      6:
      decrease(k);
      if (k==0) {
        7: return;
      }
      set(p,u_k);
      do5 = true;
    }
}

答案 3 :(得分:2)

我在https://stackoverflow.com/a/36661381/120163

之前勾画了一个OP算法

找到一篇更好的论文,讨论如何生成结构化代码,同时完全保留原始控制流图:

W.D Maurer, "Generalized structured programs and loop trees", Science of Computer Programming, 2007

我遵循了这个程序(在纸面上,希望我做得对,在凌晨2点40分看起来不错)。他的基本技巧是找到强连通区域(代码中的周期);这些将成为循环;然后,他通过删除边缘来打破这个循环;这最终成为一个循环反向链接(当他完成时恢复)。重复该过程直到找不到更多循环;剩下的本质上是一个带有识别循环的结构化程序。这样做是很棘手的;你真的需要一个自动程序。你的代码虽然很小,但仍然非常讨厌: - }

我在一个地方骗了。 Maurer坚持认为,即使进入循环中间,前进也是可以的。如果您购买,那么您可以完全保留CFG。如果没有,你必须处理一个循环有两个或多个入口点的情况;你的算法有这样一个循环。我通过对循环进行编码来解决这个问题,并编写了一个循环尾端片段等价物,其作用类似于跳到中间的第一次迭代,然后是循环本身。

我的注意事项有点滑稽:大多数语言没有"阻止{...}"结构体。 [我编码的那个(参见bio)确实]。可以把它想象成一个"执行一次迭代" loop: - }我假设块/循环有循环退出并且循环继续。如果你没有这些,你可以使用足够数量的块{...}和exit_block @ N来模拟它们。

接受后编辑:在白天,我没有做对,我遗漏了@ 3的while循环。我修补了那个;现在对块结构的需求消失了,因为我可以退出while循环@ 3来实现相同的效果。实际上,代码读得更好。

我将数字标签留在了,即使它们不需要,也可以方便参考。

//Algorithm X;
1:
initialize();
2:
while (true) {
   enter_level(k);
   3: 
   while (true) {
      set(a[k],q);
      if (test() == ok) {
         if (k != n) exit_while@3;
         visit();
         decrease(k); // replicate logic at 6 to avoid jumping into middle of 5 loop
         if (k==0) return;
         set(p,u_k);
      }
      5:
      while (true) {
         increasev2(a[k]);
         if (q != 0) continue_while@3;
         6:
         decrease(k);
         if (k==0) return;
         set(p,u_k);
      } // while(true)@5
  } // while(true)@3
  4:
  increase(k);
} // while(true)@2

与我迄今为止看到的大多数其他答案不同,它的运行速度与原始速度相同(没有额外的标志或标志检查)。

@ hatchet的回答很有意思; a)它同样快,b)他选择通过相同的技术处理两个进入循环,但他选择了"其他条目"作为循环顶部。他做了类似于" enter_level(k)"在标签2处操作。

有趣的是,所有这些结构似乎都没有帮助这个代码的可读性。关于"结构化程序"的整个观点令人惊讶。也许精心设计的意大利面条并不是那么糟糕: - }

答案 4 :(得分:1)

您可以使用大量变量来模拟使用if'swhile's

的gotos流程
initialize();

enterLevel = true;
executeWhile = true;

do 
{

    if (enterLevel)
    {
        enter_level(k);
    }

    enterLevel = false;

    goto4 = false;
    goto5 = false;
    goto6 = false;

    set(a[k],q);
    if(test() == ok) 
    {
        if (k == n) 
        {
            visit();
            goto6 = true;
        }
        else
        {
            goto4 = true;
        }
    }
    else
    {
        goto5 = true;
    }

    if (goto4) 
    {
        increase(k);
        enterLevel = true;
    }
    else
    {
        do
        {
            if(goto5)
            {
                increasev2(a[k]);
                goto6 = goto5 = !(q != 0); // if (q != 0) { goto6 = goto5 = false; } else { goto6 = goto5 = true; }
            }
            if(goto6)
            {
                decrease(k);
                executeWhile = !(k==0); // if (k == 0) { executeWhile = false; } else { executeWhile = true; }
                set(p,u_k);
                goto5 = true;
            }
        } while (goto5 && executeWhile);
    }
} while (executeWhile);

这个版本是否优于goto's我不能说的版本。

首先,我将所有标签完全分开。

然后我发现这里有2个循环:

1 - 
    * label 4 -> goto 2
    * label 5 -> goto 3. 

两者都是代码的顶部,但是一个执行enter_level(k)而另一个不执行。     这就是enterLevel var。

的原因
2 - 
    * label 6 -> goto 5. This goes up a little in the code, and then executes again. 

在这个循环中,有两种情况会消失:

    * label 5 -> goto 3. The same as before, but now inside a nested loop
    * label 6 -> goto 7. The way out of the outer loop.

其他变量和if只是为了控制流程。

是的,我可以使用一些休息时间(代码可能会变短), 但由于问题是关于goto的,我个人更愿意不使用它们。