879

在许多 C/C++ 宏中,我看到宏的代码被包裹在一个看似毫无意义的do while循环中。以下是示例。

#define FOO(X) do { f(X); g(X); } while (0)
#define FOO(X) if (1) { f(X); g(X); } else

我看不到do while它在做什么。为什么不直接写这个呢?

#define FOO(X) f(X); g(X)
4

9 回答 9

910

do ... whileandif ... else是为了让你的宏后面的分号总是意味着同样的事情。假设您有类似第二个宏的东西。

#define BAR(X) f(x); g(x)

现在,如果你要BAR(X);在一个if ... else语句中使用 if 语句的主体没有用大括号括起来,你会大吃一惊。

if (corge)
  BAR(corge);
else
  gralt();

上面的代码将扩展为

if (corge)
  f(corge); g(corge);
else
  gralt();

这在语法上是不正确的,因为 else 不再与 if 相关联。在宏中用大括号括起来并没有帮助,因为大括号后面的分号在语法上是不正确的。

if (corge)
  {f(corge); g(corge);};
else
  gralt();

有两种方法可以解决问题。第一种是使用逗号对宏中的语句进行排序,而不会剥夺它像表达式一样的能力。

#define BAR(X) f(X), g(X)

上面版本的 barBAR将上面的代码扩展成下面的代码,在语法上是正确的。

if (corge)
  f(corge), g(corge);
else
  gralt();

如果不是f(X)您有更复杂的代码体需要放在自己的块中,例如声明局部变量,这将不起作用。在最一般的情况下,解决方案是使用类似的东西do ... while使宏成为一个使用分号而不会混淆的单个语句。

#define BAR(X) do { \
  int i = f(X); \
  if (i > 4) g(i); \
} while (0)

您不必使用do ... while,您也可以用它来做一些事情if ... else,尽管在if ... elsean 内部扩展时会if ... else导致“悬空 else ”,这可能会使现有悬空 else 问题更难找到,如下面的代码所示.

if (corge)
  if (1) { f(corge); g(corge); } else;
else
  gralt();

关键是在悬挂分号错误的情况下用完分号。当然,在这一点上可以(并且可能应该)争辩说,最好将其声明BAR为实际函数,而不是宏。

总之,do ... while可以解决 C 预处理器的缺点。当那些 C 风格指南告诉你放弃 C 预处理器时,这就是他们担心的事情。

于 2008-09-30T17:36:35.693 回答
175

宏是预处理器将放入正版代码中的文本的复制/粘贴片段;宏的作者希望替换将产生有效的代码。

成功的三个好“技巧”:

帮助宏表现得像真正的代码

普通代码通常以分号结尾。如果用户查看代码不需要...

doSomething(1) ;
DO_SOMETHING_ELSE(2)  // <== Hey? What's this?
doSomethingElseAgain(3) ;

这意味着如果分号不存在,用户希望编译器产生错误。

但真正好的理由是,在某些时候,宏的作者可能需要用真正的函数(可能是内联的)替换宏。所以宏应该真的像一个。

所以我们应该有一个需要分号的宏。

生成有效代码

如 jfm3 的回答所示,有时宏包含多个指令。如果在 if 语句中使用宏,这将是有问题的:

if(bIsOk)
   MY_MACRO(42) ;

这个宏可以扩展为:

#define MY_MACRO(x) f(x) ; g(x)

if(bIsOk)
   f(42) ; g(42) ; // was MY_MACRO(42) ;

g无论 的值如何,都会执行该函数bIsOk

这意味着我们必须为宏添加一个范围:

#define MY_MACRO(x) { f(x) ; g(x) ; }

if(bIsOk)
   { f(42) ; g(42) ; } ; // was MY_MACRO(42) ;

生成有效代码 2

如果宏是这样的:

#define MY_MACRO(x) int i = x + 1 ; f(i) ;

我们可能在以下代码中遇到另一个问题:

void doSomething()
{
    int i = 25 ;
    MY_MACRO(32) ;
}

因为它会扩展为:

void doSomething()
{
    int i = 25 ;
    int i = 32 + 1 ; f(i) ; ; // was MY_MACRO(32) ;
}

当然,这段代码不会编译。因此,同样,解决方案是使用范围:

#define MY_MACRO(x) { int i = x + 1 ; f(i) ; }

void doSomething()
{
    int i = 25 ;
    { int i = 32 + 1 ; f(i) ; } ; // was MY_MACRO(32) ;
}

代码再次正常运行。

结合分号+范围效果?

有一个 C/C++ 习惯用法可以产生这种效果: do/while 循环:

do
{
    // code
}
while(false) ;

do/while 可以创建一个作用域,从而封装宏的代码,最后需要一个分号,从而扩展为需要一个的代码。

奖金?

C++ 编译器将优化 do/while 循环,因为它的后置条件为假这一事实在编译时是已知的。这意味着像这样的宏:

#define MY_MACRO(x)                                  \
do                                                   \
{                                                    \
    const int i = x + 1 ;                            \
    f(i) ; g(i) ;                                    \
}                                                    \
while(false)

void doSomething(bool bIsOk)
{
   int i = 25 ;

   if(bIsOk)
      MY_MACRO(42) ;

   // Etc.
}

将正确扩展为

void doSomething(bool bIsOk)
{
   int i = 25 ;

   if(bIsOk)
      do
      {
         const int i = 42 + 1 ; // was MY_MACRO(42) ;
         f(i) ; g(i) ;
      }
      while(false) ;

   // Etc.
}

然后被编译和优化为

void doSomething(bool bIsOk)
{
   int i = 25 ;

   if(bIsOk)
   {
      f(43) ; g(43) ;
   }

   // Etc.
}
于 2008-09-30T18:12:20.680 回答
56

@jfm3 - 你对这个问题有一个很好的答案。您可能还想补充一点,宏成语还可以通过简单的“if”语句防止可能更危险(因为没有错误)的意外行为:

#define FOO(x)  f(x); g(x)

if (test) FOO( baz);

扩展为:

if (test) f(baz); g(baz);

这在语法上是正确的,因此没有编译器错误,但可能会导致始终调用 g() 的意外后果。

于 2008-09-30T18:05:30.167 回答
25

上面的答案解释了这些构造的含义,但两者之间存在显着差异,没有提到。事实上,有理由更喜欢do ... while结构if ... else

if ... else构造的问题在于它不会强制您输入分号。就像在这段代码中:

FOO(1)
printf("abc");

虽然我们省略了分号(错误地),但代码将扩展为

if (1) { f(X); g(X); } else
printf("abc");

并将静默编译(尽管某些编译器可能会针对无法访问的代码发出警告)。但该printf语句永远不会被执行。

do ... while构造没有这样的问题,因为在之后唯一有效的标记while(0)是分号。

于 2012-08-03T15:21:08.423 回答
18

解释

do {} while (0)if (1) {} else确保将宏扩展为仅 1 条指令。除此以外:

if (something)
  FOO(X); 

将扩展为:

if (something)
  f(X); g(X); 

并将在控制语句g(X)之外执行。if使用do {} while (0)and时可以避免这种情况if (1) {} else


更好的选择

使用 GNU语句表达式(不是标准 C 的一部分),您有更好的方法do {} while (0)if (1) {} else解决这个问题,只需使用({})

#define FOO(X) ({f(X); g(X);})

并且此语法与返回值兼容(注意do {} while (0)不是),如下所示:

return FOO("X");
于 2014-03-23T12:04:30.807 回答
17

虽然预计编译器会优化do { ... } while(false);循环,但还有另一种解决方案不需要该构造。解决方案是使用逗号运算符:

#define FOO(X) (f(X),g(X))

甚至更具异国情调:

#define FOO(X) g((f(X),(X)))

虽然这适用于单独的指令,但它不适用于构建变量并将其用作以下内容的一部分的情况#define

#define FOO(X) (int s=5,f((X)+s),g((X)+s))

有了这个,将被迫使用 do/while 构造。

于 2009-10-10T08:27:38.950 回答
12

Jens Gustedt 的P99 预处理器库(是的,存在这种东西的事实也让我大吃一惊!)if(1) { ... } else通过定义以下内容,以一种小而重要的方式改进了结构:

#define P99_NOP ((void)0)
#define P99_PREFER(...) if (1) { __VA_ARGS__ } else
#define P99_BLOCK(...) P99_PREFER(__VA_ARGS__) P99_NOP

这样做的理由是,与do { ... } while(0)构造不同,break并且continue仍然在给定块内工作,但是((void)0)如果在宏调用之后省略分号,则会产生语法错误,否则会跳过下一个块。(这里实际上没有“悬空的 else”问题,因为else绑定到最近的if,这是宏中的那个。)

如果您对可以使用 C 预处理器或多或少安全地完成的各种事情感兴趣,请查看该库。

于 2014-11-19T16:19:45.383 回答
8

由于某些原因,我无法对第一个答案发表评论......

你们中的一些人展示了带有局部变量的宏,但没有人提到你不能只在宏中使用任何名称!总有一天它会咬用户!为什么?因为输入参数被替换到您的宏模板中。在您的宏示例中,您使用了可能最常用的变量名称i

例如当下面的宏

#define FOO(X) do { int i; for (i = 0; i < (X); ++i) do_something(i); } while (0)

用于以下函数

void some_func(void) {
    int i;
    for (i = 0; i < 10; ++i)
        FOO(i);
}

宏不会使用在 some_func 开头声明的预期变量 i,而是在宏的 do ... while 循环中声明的局部变量。

因此,永远不要在宏中使用公共变量名!

于 2011-12-21T18:42:56.580 回答
2

我不认为它被提及所以考虑这个

while(i<100)
  FOO(i++);

会被翻译成

while(i<100)
  do { f(i++); g(i++); } while (0)

注意i++宏如何计算两次。这可能会导致一些有趣的错误。

于 2008-10-18T22:04:41.047 回答