为什么要在宏中使用明显无意义的 do-while 和 if-else 语句?

Why use apparently meaningless do-while and if-else statements in macros?

提问人:jfm3 提问时间:10/1/2008 最后编辑:Lundinjfm3 更新时间:9/9/2023 访问量:115689

问:

在许多 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)
C C 预处理器 C++-FAQ

评论

3赞 Phil1970 3/9/2018
对于带有 else 的示例,我会在末尾添加一个 type 表达式......比如 ((void)0)。void
3赞 Cœur 12/12/2018
提醒该构造与 return 语句不兼容,因此该构造在标准 C 中具有更多兼容用法。在 GNU C 中,你会更喜欢我回答中描述的结构。do whileif (1) { ... } else ((void)0)

答:

982赞 jfm3 10/1/2008 #1

和在那里使它成为一个 宏后面的分号总是表示相同的意思。比方说你 有类似你的第二个宏的东西。do ... whileif ... else

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

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

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)

上面版本的 bar 将上面的代码扩展为下面的代码,这在语法上是正确的。BAR

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

如果你有一个更复杂的代码体需要放在它自己的块中,比如说声明局部变量,这是行不通的。在最一般的情况下,解决方案是使用 like 使宏成为单个语句,该语句采用分号而不会混淆。f(X)do ... while

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

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

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

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

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

评论

36赞 Steve Melnikoff 4/3/2009
这难道不是在if、while和for语句中始终使用大括号的有力论据吗?如果您始终强调这样做(例如,MISRA-C 要求这样做),则上述问题就会消失。
25赞 Stewart 5/19/2011
逗号示例应为逗号示例,否则运算符优先级可能会混淆语义。#define BAR(X) (f(X), g(X))
25赞 Steve Melnikoff 11/21/2013
@DawidFerenczy:虽然四年半前的你和我都说得很对,但我们必须生活在现实世界中。除非我们可以保证代码中的所有语句等都使用大括号,否则像这样包装宏是避免问题的简单方法。if
21赞 Chris Kline 6/23/2015
注意:该窗体比参数是宏扩展中包含的代码的 for 宏更安全,因为它不会更改 break 或 continue 关键字的行为。例如:在定义为 因为中断会影响宏的 while 循环,而不是宏调用站点的 for 循环,因此会导致意外行为。if(1) {...} else void(0)do {...} while(0)for (int i = 0; i < max; ++i) { MYMACRO( SomeFunc(i)==true, {break;} ) }MYMACRO#define MYMACRO(X, CODE) do { if (X) { cout << #X << endl; {CODE}; } } while (0)
10赞 Chris Kline 7/27/2015
@ace是一个错别字,我的意思是.我相信这确实解决了“悬而未决”的问题:请注意,.在这种情况下,悬空的 else (例如 ) 会触发编译错误。下面是一个演示它的活生生的例子void(0)(void)0(void)0if (cond) if (1) foo() else (void)0 else { /* dangling else body */ }
57赞 Michael Burr #2

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

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

if (test) FOO( baz);

扩展至:

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

这在语法上是正确的,因此没有编译器错误,但可能会产生意想不到的后果,即 g() 将始终被调用。

185赞 paercebal 10/1/2008 #3

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

有三个好的“技巧”可以做到这一点:

帮助宏像真正的代码一样运行

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

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) ;

无论 的值如何,都将执行该函数。gbIsOk

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

#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 循环,因为它的后置条件为 false 的事实在编译时是已知的。这意味着像这样的宏:

#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.
}

评论

7赞 Gnubie 8/23/2012
请注意,将宏更改为内联函数会更改一些标准的预定义宏,例如,以下代码显示了 FUNCTION 和 LINE 的变化: #include < stdio.h> #define Fmacro() printf(“%s %d\n”, FUNCTION LINE) inline void Finline() { printf(“%s %d\n”, FUNCTION, LINE); } int main() { Fmacro(); Finline();返回 0;}(粗体字词应用双下划线括起来 - 糟糕的格式化程序!
8赞 Jonathan Leffler 8/26/2013
这个答案有许多小问题,但并非完全无关紧要。例如:不正确的扩展;在扩展中应为 32。一个更复杂的问题是 的扩展是什么。另一个是 .有很多好东西,但也有一些未加点的 i 和未交叉的 t。void doSomething() { int i = 25 ; { int i = x + 1 ; f(i) ; } ; // was MY_MACRO(32) ; }xMY_MACRO(i+7)MY_MACRO(0x07 << 6)
0赞 Scott - Слава Україні 8/23/2017
@Gnubie:我猜你还在这里,你现在还没有弄清楚这一点:你可以用反斜杠在评论中转义星号和下划线,所以如果你输入它就会呈现为__LINE__。恕我直言,最好只对代码使用代码格式;例如,(不需要任何特殊处理)。P.S. 我不知道这在 2012 年是否属实;从那时起,他们对引擎进行了相当多的改进。\_\_LINE\_\___LINE__
1赞 Andrew 1/3/2019
感谢我的评论晚了六年,但大多数 C 编译器实际上并没有内联函数(这是标准允许的)inline
0赞 Snackoverflow 7/31/2023
谢谢!投票最多的答案是,您需要在无作用域的语句中正确行事。但是他们没有涵盖(而您做到了)是为什么仅仅将宏体放在一个范围内是不够的,例如.似乎您需要的真正原因是要求在语句后使用分号。while(0)if#define FOO(x) { bar(x); }while(0)
2赞 John Nilsson 10/19/2008 #4

我不认为有人提到过,所以考虑一下

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

将被翻译成

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

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

评论

18赞 Trent 11/19/2008
这与做无关......while(0) 构造。
2赞 John Nilsson 11/27/2008
真。但与宏与函数的主题以及如何编写表现为函数的宏有关......
3赞 mirabilos 12/21/2016
与上面的类似,这不是答案,而是评论。主题:这就是为什么你只使用一次东西:do { int macroname_i = (i); f(macroname_i); g(macroname_i); } while (/* CONSTCOND */ 0)
17赞 Marius 10/10/2009 #5

虽然编译器可以优化循环,但还有另一种解决方案不需要这种结构。解决方案是使用逗号运算符: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 结构。

评论

0赞 Marius 6/5/2011
谢谢,由于逗号运算符不保证执行顺序,因此这种嵌套是强制执行顺序的一种方式。
17赞 R.. GitHub STOP HELPING ICE 4/12/2012
@Marius:错误;逗号运算符是一个序列点,因此可以保证执行顺序。我怀疑您将其与函数参数列表中的逗号混淆了。
0赞 Marco A. 3/31/2015
只是想补充一点,编译器被迫保留程序的可观察行为,因此优化 do/while away 没什么大不了的(假设编译器优化是正确的)。
0赞 Marius 7/25/2016
@MarcoA。虽然你是对的,但我过去发现编译器优化,虽然完全保留了代码的功能,但通过改变在单一上下文中似乎无济于事的行,会破坏多线程算法。举个例子。Peterson's Algorithm
0赞 mirabilos 12/21/2016
这也不适用于所有类型的结构,尽管带有三元运算符的 C 和 this 具有相当强的表现力。
9赞 Mike Meyer 12/22/2011 #6

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

你们中的一些人展示了带有局部变量的宏,但没有人提到你不能在宏中使用任何名称!总有一天它会咬用户!为什么?因为输入参数已替换到宏模板中。在您的宏示例中,您使用了可能是最常用的变量名称 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 循环。

因此,切勿在宏中使用公共变量名称!

评论

0赞 Blaisorblade 12/22/2011
通常的模式是在宏的变量名称中添加下划线 - 例如 .int __i;
8赞 R.. GitHub STOP HELPING ICE 4/12/2012
@Blaisorblade:实际上这是不正确和非法的C;前导下划线保留供实现使用。您看到这种“通常模式”的原因是读取系统标头(“实现”),这些标头必须将自己限制在此保留命名空间中。对于应用程序/库,您应该选择自己晦涩难懂、不太可能发生冲突的名称,不带下划线,例如 或类似。mylib_internal___i
2赞 Blaisorblade 4/13/2012
@R..你是对的 - 我实际上已经在“应用程序”(Linux 内核)中读到过这个,但无论如何它都是一个例外,因为它没有使用标准库(从技术上讲,它是“独立”的 C 实现而不是“托管”的)。
4赞 Alex Celeste 5/3/2014
@R..这并不完全正确:前导下划线后跟大写或第二个下划线是为所有上下文中的实现保留的。前导下划线后跟其他内容不会保留在本地范围内。
1赞 R.. GitHub STOP HELPING ICE 5/3/2014
@Leushenko:是的,但这种区别非常微妙,我觉得最好告诉人们不要使用这样的名字。了解其中微妙之处的人大概已经知道我在掩盖细节。:-)
25赞 Yakov Galka 8/3/2012 #7

上面的答案解释了这些结构的含义,但两者之间存在未提及的显着差异。事实上,比起构造,更喜欢 是有原因的。do ... whileif ... else

该结构的问题在于它不会强迫您输入分号。就像在这段代码中一样:if ... else

FOO(1)
printf("abc");

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

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

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

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

评论

3赞 Yakov Galka 8/4/2012
@RichardHansen:仍然没有那么好,因为从宏调用的角度来看,你不知道它是扩展到语句还是表达式。如果有人假设后者,她可能会写信,这将再次给我们一个误报。只需使用,仅此而已。FOO(1),x++;do ... while
1赞 Richard Hansen 8/4/2012
记录宏以避免误解就足够了。我确实同意这是可取的,但它有一个缺点:A or 将控制循环,而不是包含宏调用的循环。所以这个伎俩仍然有价值。do ... while (0)breakcontinuedo ... while (0)if
3赞 Patrick Schlüter 12/22/2012
我看不出你可以在哪里放置一个或一个会被视为在你的宏伪循环中。即使在宏参数中,它也会出现语法错误。breakcontinuedo {...} while(0)
7赞 Patrick Schlüter 12/22/2012
使用而不是构造的另一个原因是它的惯用性质。该结构很普遍,众所周知,并被许多程序员大量使用。其基本原理和文件是众所周知的。对于构造来说,情况并非如此。因此,在进行代码审查时,需要花费更少的精力来思考。do { ... } while(0)if whateverdo {...} while(0)if
2赞 Richard Hansen 7/19/2013
@tristopia:我见过有人编写将代码块作为参数的宏(我不一定推荐这样做)。例如:。它可以像 一样使用,其中 用于指代包含调用的循环。#define CHECK(call, onerr) if (0 != (call)) { onerr } else (void)0CHECK(system("foo"), break;);break;CHECK()
21赞 Cœur 3/23/2014 #8

解释

do {} while (0)并确保宏扩展到仅 1 条指令。否则:if (1) {} else

if (something)
    FOO(X); 

将扩展到:

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

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


更好的选择

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

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

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

return FOO("X");

评论

5赞 Alexander Stohr 12/21/2018
在宏中使用块钳制 {} 足以捆绑宏代码,以便为相同的 if 条件路径执行所有代码。do-while around 用于在使用宏的位置强制使用分号。因此,宏被强制执行,其功能更相似。这包括使用时对尾部分号的要求。
12赞 Isaac Schwabacher 11/20/2014 #9

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)breakcontinue((void)0)elseif

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

评论

0赞 Segmented 3/27/2015
虽然非常聪明,但这会导致编译器警告轰炸潜在的悬而未决的其他人。
2赞 mirabilos 12/21/2016
你通常使用宏来创建一个包含的环境,也就是说,你永远不会在宏中使用(或)来控制在外部开始/结束的循环,这只是糟糕的风格,隐藏了潜在的出口点。breakcontinue
0赞 Rene 6/1/2017
Boost 中还有一个预处理器库。它有什么令人兴奋的?
0赞 melpomene 6/8/2019
风险在于,有人可能正在写作,并且在语法上是有效的,但永远不要调用 .这是一个语法错误。else ((void)0)YOUR_MACRO(), f();f()dowhile
0赞 Carl Lei 8/15/2019
@melpomene那呢?else do; while (0)