做{…} while(false)


125

我正在查看某个人的一些代码,发现他的功能似乎具有某种模式:

<return-type> function(<params>)
{
 <initialization>

 do
 {
   <main code for function>
 }
 while(false);

 <tidy-up & return>
}

这还不错,更奇特(实际代码相当简洁且毫不奇怪)。这不是我以前见过的东西,我想知道是否有人能想到它背后的任何逻辑-也许是另一种语言的背景?


2
您是否尝试过将其更改为“普通”版本并查看其是否仍可以编译?如果正常编写后无法编译,则编译器错误可能会提示执行此操作的原因。
Mark Byers

11
为什么不问“个人”而不是问我们关于他意图的猜测呢?

1
也许他的教授要求他使用a,do...while()并且从那以后他就一直使用该代码作为函数模板。
霍根

6
@Neil ...当我面对他时,我想避免显得愚蠢,他解释了我从未想过的超高级C ++用法。
男孩先生

6
似乎已经有很多不同的答案,您可以问一下原作者的意图并发回去吗?
nevets1219 2010年

Answers:


174

您可以break退出do{...}while(false)


95
+1是因为这可能是代码的意图,但是执行类似的操作只是愚蠢的伪装了goto。如果你觉得一个goto是这个职位的合适的工具,那么你应该只使用一个#$(*#@页转到
dsimcha

55
它不仅仅是伪装的goto。这是受限制的(结构化)goto。
托马斯·爱丁

19
它以什么方式被“限制”?仅向前跳跃几乎不是“限制”。goto是goto,对它进行修饰以使其看起来像不是刚开始时仅使用goto会更糟。
Anon。

44
@Anon:向前跳是goto的限制,而跳出肯定是一个限制。gotos的真正问题是意大利面条代码,并且向前和向后的跳跳限制了这一点。
David Thornley,2010年

33
实际的循环在语义上不是goto。有条件的在语义上不是goto。“转到函数的结尾并做清理代码” 语义goto语句。在应用语义时请使用gotos,不要修饰语义上不同的内容,因为您会害怕它们。
Anon。

125

许多人指出,断点通常用作编写“ goto”的尴尬方式。如果直接在函数中编写,那可能是正确的。

在宏OTOH中,这do { something; } while (false)是在宏调用后强制分号的一种便捷方法,绝对不允许跟随其他令牌。

另一种可能性是,曾经有一个循环,或者预计将来会增加迭代(例如,在测试驱动的开发中,不需要迭代来通过测试,但是从逻辑上讲,如果该功能需要比当前要求更通用)


18
+1用于在宏中提及此功能;我很惊讶没有其他人提到!
尼克·迈耶

7
是的,宏实际上是对此的一种完全有效的用法。当然,在外部宏中,这只是愚蠢的...;)
jalf

12
这并不尴尬,但很有用,因为它是作用域的 goto-这意味着您在do循环中声明的所有变量都会被销毁,而goto不会这样做。
安娜·贝茨

9
@Paul:没有什么可以阻止您在一堆语句周围加上花括号来强制goto这种行为。
erikkallen

5
@Paul:走出一个块最肯定会导致局部变量在C ++中被破坏,与break相同。在C变量中,并没有真正销毁它们,只是简单地回收了它们的堆栈空间。
Ben Voigt 2010年

25

转到go的突破可能是答案,但是我将提出另一个想法。

也许他想拥有一个局部定义的变量,并使用此构造来获得新的作用域。

请记住,尽管最近的C ++允许在{...}任何地方使用,但并非总是如此。


30
在那种情况下,他可能只用了花括号。
Nemanja Trifunovic

2
@Nemanja,您会惊讶于有多少开发人员不知道这一点,并尝试与Hogan提出的建议相关的东西
Polaris878 2010年

8
@Polaris @Nemanja,直到我发现用C / C ++编程已有4年之久,我才知道您可以{} 在任何地方创建一个新的本地范围。.这在switch-case代码中特别方便
Earlz 2010年

1
@Nemanja:我不知道确切的时间,但是我确定{...}任何地方都是C ++的更现代的开发(记住我使用的第一个C ++是通过预处理器实现的,并且不允许这种现代用法。)作者当时学校很老。
霍根

2
什么时候不允许到处都有括号?我大约15年前开始编程,然后被允许使用(无论是在我的教科书中还是在我尝试过的每个编译器中)。
erikkallen 2010年

20

当函数有许多潜在的退出点时,我已经将其用作有用的模式,但是无论函数如何退出,始终需要相同的清除代码。

通过仅在到达出口点时就必须断开,其余逻辑可以内联,它可以使烦人的if / else-if树更容易阅读。

此模式在没有goto语句的语言中也很有用。也许那是原始程序员学习模式的地方。


15
然后,只需使用诚实,直接的goto而不是伪装成goto的goto。
dsimcha'2

4
我更喜欢这种方式。它很容易阅读,并且没有goto的污名。
卡梅伦

8
如果在本地很少使用,gotos非常易于阅读。当他们成为流量控制的主要形式时,他们从后面受到了耻辱,并且跨越了数百条线。
dsimcha'2

13
由于存在“污名”而没有使用goto,这无疑是易学编程的标志。
Anon。

6
更不用说大量的清除代码是难闻的气味。这是C ++,清理代码通常应在RAII处理期间调用的析构函数中。
David Thornley,2010年


10

这只是whilegoto tidy-up不使用goto 的情况下获得语义的变态。

这是一种不好的形式,因为当您在外部使用其他循环时,对读者来说whilebreaks变得模棱两可。 “这是应该退出吗?还是仅是为了摆脱内循环?”


10

我认为写break代替会更方便goto end。您甚至不必考虑标签的名称,这将使意图更清晰:您不想跳转到具有特定名称的标签。您想离开这里。

同样有可能您仍然需要使用牙套。所以这是do{...}while(false);版本:

do {
   // code
   if (condition) break; // or continue
   // more code
} while(false);

如果要使用它,这就是表达方式goto

{
   // code
   if (condition) goto end;
   // more code
}
end:

我认为第一个版本的含义容易掌握。此外,它更容易编写,更易于扩展,更容易翻译成不支持的语言goto,等等。


关于的使用,最常被提及的担心break是它的伪装严重goto。但实际上实际上break更像return:两条指令都跳出了一个代码块,与之相比,代码块的结构化程度更高goto。尽管如此,两条指令都允许在代码块中有多个出口点,这有时会造成混淆。毕竟,无论具体情况如何,我都会尝试寻求最清晰的解决方案。


哦,我只是注意到在回答之前我并没有完全理解问题。我认为这是关于do{ ... }while(false);一般的使用。但是实际上,它是关于使用它来模拟某种try{ ... }finally{ ... }
罗伯特

很好地观察了中断和返回之间的平行关系,而不是转到。同意最清晰的解决方案是最好的。在许多情况下,将这样的代码封装在自己的单独函数中可能更清晰,该函数使用return而不是break,然后在调用函数中进行清理。
威胁

7

那些不愿goto在代码中使用显式的程序员使用此技巧。以上代码的作者希望能够直接从代码中间跳转到“清除并返回”点。但是他们不想使用标签和显式的goto。取而代之的是,他们可以使用break上述“假”循环的主体内部来达到相同的效果。


7

这是非常普遍的做法。在C中。我试着以为您似乎想以“我不使用” 的方式对自己撒谎goto。考虑一下,goto类似的用法不会有任何问题。实际上,这也会降低缩进级别。

就是说,尽管如此,我注意到,这种do..while循环经常会增长。然后它们进入ifs和elses内,使代码实际上不是很可读,更不用说可测试了。

这些do..while通常旨在进行清理。我将尽可能使用RAII并从short函数中尽早返回。另一方面,C不能像C ++那样为您提供很多便利,这是进行清理的最佳方法之一。do..while


6

看起来像C程序员。在C ++中,自动变量具有用于清除的析构函数,因此在返回之前不需要整理任何东西。在C语言中,您没有这个RAII习惯用语,因此,如果您有通用的清理代码,则可以使用goto它,也可以使用上述的一次性循环。

与C ++习惯用法相比,它的主要缺点是,如果在体内抛出异常,它将无法整理。C没有异常,所以这不是问题,但这确实使它成为C ++的坏习惯。


4

几种解释。第一个是通用的,第二个特定于具有参数的C预处理器宏:

流量控制

我已经看到了在纯C代码中使用的代码。基本上,它是goto的安全版本,因为您可以脱离它,并且所有内存都可以正确清理。

为什么会有类似的东西goto好?好吧,如果你有代码,其中几乎每行可以返回一个错误,但你需要作出反应,他们都以同样的方式(例如,通过移交这个错误给调用者清理后),它通常是更具可读性,以避免if( error ) { /* cleanup and error string generation and return here */ }为它避免了重复的清理代码。

但是,在C ++中,正是出于这个目的,有例外+ RAII,所以我认为它是不好的编码风格。

分号检查

如果在函数调用宏之后忘记了分号,则参数可能会以不希望的方式收缩并编译为有效的语法。想象一下宏

#define PRINT_IF_DEBUGMODE_ON(msg) if( gDebugModeOn ) printf("foo");

那是偶然地称为

if( foo )
    PRINT_IF_DEBUGMODE_ON("Hullo\n")
else
    doSomethingElse();

在“其他”会被认为是与相关的gDebugModeOn,所以,当foofalse时,完全相反的意图是什么会发生。

提供临时变量的范围。

由于do / while带有花括号,因此临时变量具有明确定义的范围,它们无法转义。

避免“可能不需要的分号”警告

某些宏仅在调试版本中被激活。您可以像这样定义它们:

#if DEBUG
#define DBG_PRINT_NUM(n) printf("%d\n",n);
#else
#define DBG_PRINT_NUM(n) 
#endif

现在,如果您在条件内部的发布版本中使用此代码,它将编译为

if( foo )
    ;

许多编译器将此视为与

if( foo );

这通常是偶然写的。因此,您会收到警告。do {} while(false)将其从编译器中隐藏,并被编译器接受,以表明您确实不想在此处执行任何操作。

避免被条件捕获

上例中的宏:

if( foo )
    DBG_PRINT_NUM(42)
doSomething();

现在,在调试版本中,由于我们习惯性地也包括了分号,因此编译就可以了。但是,在发布版本中,它突然变成:

if( foo )

doSomething();

或更清晰的格式

if( foo )
    doSomething();

这根本不是预期的。在宏周围添加do {...} while(false)会将丢失的分号转换为编译错误。

这对OP意味着什么?

通常,您希望使用C ++中的异常进行错误处理,并使用模板而不是宏。但是,在极少数情况下,您仍然需要宏(例如,使用令牌粘贴生成类名时)或仅限于纯C语言,这是一种有用的模式。


关于作用域,您不需要循环设备;“未经修饰”的块是合法的:x =1; y = 2; { int tmp = y; y = x; x = tmp; }
chepner

但是,未经修饰的块并不会强制缺少分号,这可能会带来不良后果。做{} while(); 需要分号,因此例如,如果宏在发行版本中未定义任何内容,则不会导致以下语句被绘制为“ if”,例如上面的示例。
uliwitness

不能通过为宏提供更好的定义来避免这种情况吗?#define DBG_PRINT_NUM(n) {}
chepner

1
否,因为{}是完整语句,即等同于“;”。因此,如果您编写if(a) DBG_PRINT_NUM(n); else other();,它将无法编译。仅if(a) {} else other();if(a) ; else other();有效,但if(a) {}; else other();不是有效的,因为使得“ if”子句包含两个语句。
uliwitness

2

也许可以使用它,以便break可以在内部随时中止进一步代码的执行:

do {
    if (!condition1) break;
    some_code;
    if (!condition2) break;
    some_further_code;
    // …
} while(false);

7
这样做似乎是为了避免goto仅仅因为有人听到它“不好”而尝试使用它。
Anon。

1
也许吧,但是C ++出于这个原因有例外。
TED


2

很简单:显然,您可以随时使用该break语句跳出伪循环。此外,该do块是一个单独的范围(也可以{ ... }仅通过它来实现)。

在这种情况下,使用RAII(函数结束时对象会自动正确销毁)可能是一个更好的主意。另一个类似的构造是使用goto- 是的,我知道这很邪恶,但是可以将其用于具有如下所示的通用清理代码:

<return-type> function(<params>)
{
 <initialization>

 <main code for function using "goto error;" if something goes wrong>

 <tidy-up in success case & return>

 error:

 <commmon tidy-up actions for error case & return error code or throw exception>
}

(顺便说一句:Lua中使用了do-while-false构造来弥补丢失的continue语句。)


2

许多回答者给出了原因do{(...)break;}while(false)。我想用另一个真实的例子来补充图片。

在以下代码中,我必须operation根据data指针指向的地址设置枚举数。因为开关盒只能先用于标量类型,所以我这样做的效率很低

if (data == &array[o1])
    operation = O1;
else if (data == &array[o2])
    operation = O2;
else if (data == &array[on])
    operation = ON;

Log("operation:",operation);

但是由于Log()和其余的代码会针对任何选定的操作值重复执行,因此我一直在徘徊如何在已经发现地址后如何跳过其余的比较。这就是do{(...)break;}while(false)派上用场的地方。

do {
    if (data == &array[o1]) {
        operation = O1;
        break;
    }
    if (data == &array[o2]) {
        operation = O2;
        break;
    }
    if (data == &array[on]) {
        operation = ON;
        break;
    }
} while (false);

Log("operation:",operation);

有人可能想知道为什么他不能breakif声明中这样做,例如:

if (data == &array[o1])
{
    operation = O1;
    break;
}
else if (...)

break仅与最接近的封闭交互循环或切换时,无论是一个forwhiledo .. while类型,所以很遗憾这是行不通的。


1

作者几岁?

我之所以问,是因为我曾经在80年代末期遇到过执行此操作的实时Fortran代码。事实证明,这是在没有线程的操作系统上模拟线程的一种很好的方法。您只需将整个程序(您的调度程序)放入一个循环中,然后一个接一个地调用您的“线程”例程。线程例程本身就是循环,直到发生多种情况之一(通常是一定数量的情况)时间已经过去了),这是“合作多任务”,这取决于各个线程是否时不时放弃CPU,这样其他线程就不会饿死。您可以嵌套循环子程序调用以模拟线程优先级乐队。



0

我同意大多数海报关于将其伪装成goto的用法。宏也被认为是以这种方式编写代码的潜在动机。

我还看到这种结构在C / C ++混合环境中使用是穷人的例外。如果通常会在循环中遇到异常的情况,则带有“ break”的“ do {} while(false)”可用于跳到代码块的末尾。

我也曾在强制执行“每功能单收益”意识形态的商店中使用过这种构造。同样,这代替了显式的“ goto”方法,但是其动机是避免多个返回点,而不是“跳过”代码并继续在该函数中实际执行。


0

我使用Adobe InDesign SDK,并且InDesign SDK示例几乎具有所有这样编写的功能。由于事实,该功能通常很长。您需要执行QueryInterface(...)从应用程序对象模型中获取任何内容的地方。因此,通常每个QueryInterface都跟不上if,中断。


0

许多人已经说明了此构造与a之间的相似性goto,并表示倾向于goto。也许此人的背景包括编码准则严格禁止goto的环境?


0

我能想到的另一个原因是它装饰了花括号,而我相信在较新的C ++标准中,裸花括号不好(ISO C不喜欢它们)。否则,使静态分析仪(如棉绒)安静下来。

不确定为什么要使用它们,可能是可变作用域还是调试器的优点。

请参见琐碎的“ Do While循环”,以及C2中的花括号很好

为了阐明我的术语(我相信遵循标准用法):

裸牙套

init();
...
{
c = NULL;
mkwidget(&c);
finishwidget(&c);
}
shutdown();

空括号(NOP):

{}

例如

while (1)
   {}  /* Do nothing, endless loop */

区块

if (finished)
{
     closewindows(&windows);
     freememory(&cache);
}

会变成

if (finished)
     closewindows(&windows);
freememory(&cache);

如果删除括号,从而改变执行流程,而不仅是局部变量的范围。因此不是“独立式”或“裸露式”。

可以使用裸括号或块来表示代码的任何部分,这些部分可能是您希望标记的(内联)函数的潜在功能,但此时不进行重构。


真?真可惜 我对C感兴趣的几件事之一是,它使您几乎可以在任何地方声明一个新的嵌套作用域。
TED

1
裸括号有时会有助于修复奇怪的崩溃。参见blogs.msdn.com/oldnewthing/archive/2004/05/20/135841.aspx
Brian

1
在C ++中,可以在允许单个语句的任何地方使用块(即“裸括号”)。
Ben Voigt 2012年

@BenVoigt空括号,即NOP与“裸括号”不同,后者是围绕线性指令序列添加的块。例如`printf(“ Hello”); {putchar(','); putchar(0x20); } printf(“ world!\ n”);`括号属于循环或分支控制的一部分。
mctylr 2012年

@mctylr:我不是在说空括号。
Ben Voigt 2012年

0

这是一种模仿 GOTO因为这两个实际上是相同的:

// NOTE: This is discouraged!
do {
    if (someCondition) break;
    // some code be here
} while (false);
// more code be here

和:

// NOTE: This is discouraged, too!
if (someCondition) goto marker;
// some code be here
marker:
// more code be here

另一方面,这两者实际上都应使用ifs 完成:

if (!someCondition) {
    // some code be here
}
// more code be here

虽然如果您将一长串的前移字符串嵌套会变得有些丑陋,GOTO转换为嵌套if的,。真正的答案是适当的重构,而不是模仿古老的语言构造。

如果您拼命尝试用 GOTO s,则可以使用此惯用法来实现。但是,这肯定是非标准的,并且是一个很好的指示,表明您没有严格遵守该语言的预期惯用法。

我不知道任何类似C的语言,其中do / while实际上是任何事物的惯用解决方案。

您可能会将整个混乱重构为更明智的方式,以使其更加惯用且更具可读性。


1
艾伦,do..while(false)不是运行的时间“不确定”号,它是运行一次
德米特里(Dmitry)2010年

2
如果您最后有一条条件continue语句(如我所示),它将运行不确定的次数。通过undefined我只是说你不知道它的运行超过一次,除非你能预测的条件是否会在特定的迭代中得到满足。没有任何continue在IT方面,do..while(false)将运行一次,也没有任何break在IT方面,while(true)将永远运行下去,所以“默认行为”是不明白什么真正相关的可以用循环来完成。
艾伦·梅

当您定义具有多个语句的宏时,这很有用。如果将它们包装在do / while(false)中,则可以像其他函数调用一样在if / else语句中使用该宏。有关说明,请参见noveltheory.com/TechPapers/while.htm
John Paquin

5
有人不了解的语义continuecontinue不重复循环。“该continue语句应仅在迭代语句中出现,并使控制传递到最小的封闭迭代语句的循环继续部分,即循环的末尾。”
Ben Voigt

-1,由于@BenVoigt指出的原因,前两个语句并不相同。
cmh 2013年

0

一些编码器更喜欢只从其功能中退出/返回一次。使用假人做{....} while(false); 完成后,您仍然可以“脱离”虚拟循环,并且仍然只有一次返回。

我是一名Java编码器,所以我的示例将是

import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class p45
{
    static List<String> cakeNames = Arrays.asList("schwarzwald torte", "princess", "icecream");
    static Set<Integer> forbidden = Stream.of(0, 2).collect(Collectors.toSet());

    public static  void main(String[] argv)
    {
        for (int i = 0; i < 4; i++)
        {
            System.out.println(String.format("cake(%d)=\"%s\"", i, describeCake(i)));
        }
    }


    static String describeCake(int typeOfCake)
    {
        String result = "unknown";
        do {
            // ensure type of cake is valid
            if (typeOfCake < 0 || typeOfCake >= cakeNames.size()) break;

            if (forbidden.contains(typeOfCake)) {
                result = "not for you!!";
                break;
            }

            result = cakeNames.get(typeOfCake);
        } while (false);
        return result;
    }
}


-1

这很有趣。正如其他人所说的,循环中可能会有中断。我本可以这样做:

while(true)
{
   <main code for function>
   break; // at the end.
}

2
有永远循环的潜力吗?do..while(false)总是退出,while(true)更有风险。
德米特里(Dmitry)2010年

1
while(true)是大多数语言中正确的习惯用法。您经常会在GUI应用程序中找到它作为程序的主循环。因为您基本上是假设程序在被告知这样做之前不应该死亡,所以a do..while(false)会导致各种人为的逻辑。这种方法可能更受完美主义者POV的欢迎,但从语义上讲更容易,因此对于人类程序员(对不起,Skynet)来说,更容易出错。
艾伦·梅

2
@dmitry do{...}while(false)while(true){ .. break;}
N 1.1 2010年

4
@ N1.1:不在时continue,它们是不同的。
Ben Voigt 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.