do { ... } while (0) - 它有什么好处?

430

我已经看到这个表达式十多年了。我一直在思考它的用途。由于我主要在#define中看到它,我认为它适用于内部作用域变量声明和使用break(而不是goto)。

除此之外,它还有什么好处吗?您使用它吗?


请看这个问题 - Federico A. Ramponi
13
实际上,这不是一个重复的问题,因为链接的问答与定义无关。比较两个答案很容易就能发现它们并不相同。 - Doomsday
请查看 Redis 的 "decrement_used_memory",位于第 53 行 [链接]https://github.com/antirez/redis-tools/blob/master/zmalloc.c - yet
重复的问题是标记为可能重复的问题(帖子的第一行),而不是由Federico A. Ramponi提出的问题。 - Étienne
建议的重复问题仅涉及宏的使用。而此问题则涵盖了一般用法。 - dcow
2
其他变体的 do { ... } while ((void)0, 0) 用于消除编译器关于“常量条件”的警告。 - Amro
5个回答

615

在C语言中,这是唯一一个可以用于#define多个语句操作的结构,你可以在其中加上分号,并仍然可以在if语句中使用。下面是一个示例:

#define FOO(x) foo(x); bar(x)

if (condition)
    FOO(x);
else // syntax error here
    ...;

即使使用花括号也无济于事。
#define FOO(x) { foo(x); bar(x); }

if 语句中使用此语句将要求您省略分号,这是不符合直觉的:

if (condition)
    FOO(x)
else
    ...

如果您这样定义FOO:
#define FOO(x) do { foo(x); bar(x); } while (0)

那么以下语法是正确的:

if (condition)
    FOO(x);
else
    ....

8
即使不太美观,#define FOO(x) if(true){ foo(x); bar(x); } else void(0) 这个定义也能够工作吗? - Adisak
6
如果你的宏展开是一个表达式列表,那么这个方法可行。但是,如果你想在展开中包含“if”或“while”,这个技巧也不适用。 - Greg Hewgill
25
我认为这段代码仍然很丑陋、懒散,并且如果正确编码可以避免。这涉及到一些常识性的C语言规则,就是在IF语句中总是使用花括号,即使在IF之后只有一个操作。我认为这应该成为标准做法。 - LarryF
76
问题在于,如果你正在编写一个供他人使用的头文件,你无法选择其他人如何使用你的宏定义。为了避免给用户带来意外惊喜,你必须使用像上面提到的技术使你的宏定义表现得像任何其他C语句一样。 - Greg Hewgill
6
@AaronFranke 因为 {...};实际上由两个语句组成,其中包括{...}部分和随后的另一个空语句。但do {...} while(0); 是一个语句。Larry / Marco 当用户编写 FOO(a, b, c) 时,他们期望它(除非有明确意图)产生一个表达式,就像调用无返回值函数一样。你们的方法实际上是偷懒的做法。你应该不必关心用户的代码风格约定。 - yonil
显示剩余17条评论

137

这是一种简化错误检查并避免深度嵌套if语句的方法。例如:

do {
  // do something
  if (error) {
    break;
  }
  // do something else
  if (error) {
    break;
  }
  // etc..
} while (0);

19
将其拆分为一个方法并使用早期返回。 - Dustin Getz
17
或者...只需使用 do { } while(0)。 - nickf
86
或者使用 goto。不,我是认真的,对我来说,if (error) goto error; 和在结尾处附带一个 error: ... 看起来更简洁,也能达到相同的效果。 - FireFly
6
@WChargin在你链接的文章中提到的“goto fail”代码,即使使用“break”也会失败。有人只是在那里复制了一行代码。这不是goto的错。 - Niccolo M.
6
@NiccoloM.:“不错,GOTO并不是错误,这使得情况更加滑稽可笑。” - wchargin
显示剩余3条评论

113

将多个语句组合成一个可以使用函数式宏的单个语句块有助于使其像函数一样使用。假设您有以下代码:

#define FOO(n)   foo(n);bar(n)

然后你执行:

void foobar(int n) {
  if (n)
     FOO(n);
}

然后这就扩展为:

void foobar(int n) {
  if (n)
     foo(n);bar(n);
}

请注意第二个调用bar(n)不再是if语句的一部分。

将它们都包裹在do { } while(0)中,您也可以在if语句中使用该宏。


4
非常清晰的回答。+1 - Anirudh Ramanathan
2
清楚理解。;) - Whoami
为什么不在它周围加上大括号呢? #define FOO(n) {foo(n);bar(n)} - endolith
4
因为#define FOO(n) {foo(n);bar(n)}会被展开成{foo(n);bar(n)}。请注意,在bar(n)后面没有分号。 - John

23

有趣的是,在以下情况下,do {} while (0) 循环无法正常工作:

如果你想要一个类似函数的宏来返回值,那么你需要使用 语句表达式: ({stmt; stmt;}) 而不是 do {} while(0):


#include <stdio.h>

#define log_to_string1(str, fmt, arg...) \
    do { \
        sprintf(str, "%s: " fmt, "myprog", ##arg); \
    } while (0)

#define log_to_string2(str, fmt, arg...) \
    ({ \
        sprintf(str, "%s: " fmt, "myprog", ##arg); \
    })

int main() {
        char buf[1000];
        int n = 0;

        log_to_string1(buf, "%s\n", "No assignment, OK");

        n += log_to_string1(buf + n, "%s\n", "NOT OK: gcc: error: expected expression before 'do'");

        n += log_to_string2(buf + n, "%s\n", "This fixes it");
        n += log_to_string2(buf + n, "%s\n", "Assignment worked!");
        printf("%s", buf);
        return 0;
}

8
这是GCC的扩展功能。然而在C++11中,您可以使用lambda表达式完成相同的操作。 - celticminstrel
TIL语句表达式。很好。 使用lambda会怎样呢?我很好奇。Lambda返回一个函数对象,本质上,您仍然需要调用它,然后从宏中返回结果。lambda和调用都需要形成单个语句。你怎么做?运算符,? - yonil

-7
通常来说,do/while 循环适用于任何需要至少执行一次循环的情况。虽然可以通过简单的 while 或者 for 循环来模拟这样的循环,但结果通常会略显不够优美。我承认,这种模式的具体应用相当罕见,但确实存在。其中一个我想到的例子是基于菜单的控制台应用程序:
do {
    char c = read_input();

    process_input(c);
} while (c != 'Q');

1
它也可以在C#中使用,但C#没有宏。我不确定为什么有人会对这个回复进行负评,但我认为它几乎是正确的答案,只是忽略了while条件中明确的“0”。请各位,如果你们要对某人的回复进行负评,请留言说明原因。 - Jon Davis
23
我不是那个给问题点踩的人;然而,这个问题非常具体,答案虽然一般正确但缺乏背景。 - tzot
2
该语句是正确的,但它缺乏上下文。 - Nadeem Khan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接