这个问题的灵感来自于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之后立即看到的内容。
我对如何重构一步一步感兴趣,而不仅仅是代码。
注意:
goto
跳转实际实现算法X是直截了当的。实现黑盒函数initialize()
等只需要一些额外的指令,但这些指令与代码的结构无关。在函数调用期间发生的事情并不重要,因为现在的重点是程序的流程。答案 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;
}
更改使用以下算法:
摆脱未使用的标签。删除lbl1
如果标签以goto结尾,则将该块替换为使用它的任何位置。
移除lbl4
,lbl6
,lbl7
如果标签返回自身,则将块放入while(true)。
移除底部lbl5
(lbl5
现已自包含,可在使用时替换)
如果块是自包含的,则替换它使用的任何位置。
删除lbl5
如果一个标签跟随另一个标签,则在块的末尾放置一个转到下一个标签,以便可以按规则2替换它。
移除lbl2
(可以goto lbl3
)
现在,我们在整个代码中留下了最后一个标签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's
和while's
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的,我个人更愿意不使用它们。