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


787

在许多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)

2
对于else的示例,我将void在末尾添加一个类型的表达式...例如((void)0)
Phil1970年

1
提醒您该do while构造与return语句不兼容,因此该if (1) { ... } else ((void)0)构造在Standard C中具有更多的兼容用法。在GNU C中,您将更喜欢我的答案中所述的构造。
心教堂

Answers:


829

do ... whileif ... 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)

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

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 ... else在内部扩展时会if ... else导致“ 悬空 ”,这可能使现有悬空的问题更加难以发现,如以下代码所示。

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

关键是在悬挂的分号错误的情况下用完分号。当然,在这一点上可以(也许应该)认为,将其声明BAR为实际函数而不是宏会更好。

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


18
在if,while和for语句中始终使用花括号不是一个强有力的论点吗?如果您始终要这样做(例如,对于MISRA-C是必需的),那么上述问题就会消失。
史蒂夫·梅尔尼科夫

17
逗号示例应为#define BAR(X) (f(X), g(X))否则,运算符优先级可能会使语义混乱。
斯图尔特

20
@DawidFerenczy:尽管您和我(四年半前)都说得很对,但我们必须生活在现实世界中。除非我们可以保证代码中的所有if语句等都使用花括号,否则像这样包装宏是避免问题的一种简单方法。
史蒂夫·梅利尼克诺夫

8
注意:该if(1) {...} else void(0)格式比do {...} while(0)for宏更安全,因为for宏的参数是宏扩展中包含的代码,因为它不会更改break或continue关键字的行为。例如: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)因为中断影响宏的while循环,而不是宏调用站点上的for循环。
克里斯·克莱恩

5
void(0)我的意思是@ace 是一个错字(void)0。我相信这确实解决了“其他问题”:请注意,在后面没有分号(void)0。在这种情况下悬空(例如if (cond) if (1) foo() else (void)0 else { /* dangling else body */ })会触发编译错误。这是一个生动的例子来演示
克里斯·克莱恩

153

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

要成功,有三个好的“技巧”:

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

普通代码通常以分号结尾。用户是否应该查看不需要的代码...

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循环,因为在编译时就知道其后置条件为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.
}

6
请注意,将宏更改为内联函数会更改一些标准的预定义宏,例如,以下代码显示了FUNCTIONLINE的更改:#include <stdio.h> #define Fmacro()printf(“%s%d \ n”,FUNCTIONLINE)inline void Finline(){printf(“%s%d \ n”,FUNCTIONLINE); } int main(){Fmacro(); Finline(); 返回0; }(粗体字应加双下划线-格式错误!)
Gnubie 2012年

6
这个答案有很多小问题,但不是完全无关紧要的问题。例如:void doSomething() { int i = 25 ; { int i = x + 1 ; f(i) ; } ; // was MY_MACRO(32) ; }不是正确的扩展;将x在扩张应该是32一个更复杂的问题是什么,是的扩张MY_MACRO(i+7)。另一个是的扩展MY_MACRO(0x07 << 6)。有很多好处,但也有一些未加点的i和未交叉的t。
乔纳森·勒夫勒

@Gnubie:我想您仍然在这里,但您现在还没有弄清楚:您可以在带反斜杠的注释中转义星号和下划线,因此如果您将\_\_LINE\_\_其渲染为__LINE__。恕我直言,最好只对代码使用代码格式;例如__LINE__(不需要任何特殊处理)。PS:我不知道这在2012年是否成立。从那时起,他们对引擎进行了很多改进。
斯科特(Scott)

1
感谢我的评论晚了六年,但是大多数C编译器实际上并未内联inline函数(如标准所允许)
Andrew

53

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

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

if (test) FOO( baz);

扩展为:

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

从语法上讲这是正确的,因此没有编译器错误,但是可能会意外地导致总是调用g()。


22
“可能是意料之外的”?我会说“肯定是不希望的”,否则需要将程序员拿出来开枪(而不是用鞭子将其打成圆满)。
劳伦斯·多尔

4
否则,如果他们正在一家三字母代理机构工作,并秘密地将该代码插入广泛使用的开源程序中,那么他们可能会得到加薪... :-)
R .. GitHub停止帮助ICE 2012年

2
这个评论让我想起了苹果操作系统中最新的SSL证书验证错误中的goto失败行
Gerard Sexton 2014年

23

上面的答案解释了这些构造的含义,但是两者之间有很大的不同,没有提到。实际上,有理由更喜欢do ... whileif ... else结构。

if ... else构造的问题是它不会强迫您放入分号。像下面的代码:

FOO(1)
printf("abc");

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

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

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

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


2
@RichardHansen:仍然不是很好,因为从宏调用的角度来看,您不知道它是扩展为语句还是表达式。如果有人认为以后FOO(1),x++;再做,她可能会写信,这将再次给我们带来误报。只是使用do ... while而已。
Yakov Galka '08年

1
记录宏以避免误解就足够了。我确实同意这do ... while (0)是可取的,但它有一个缺点:A breakcontinue将控制do ... while (0)循环,而不是包含宏调用的循环。因此,该if技巧仍然有价值。
理查德·汉森

2
我看不到可以在宏伪循环内放置a break或a的位置。即使在宏参数中,也会产生语法错误。continuedo {...} while(0)
PatrickSchlüter'12

5
使用do { ... } while(0)而不是if whatever构造的另一个原因是它的惯用性质。该do {...} while(0)结构是广泛的,众所周知的,并被许多程序员大量使用。其原理和文档资料是众所周知的。对于if构造不是这样。因此,在进行代码审查时,花费较少的精力来完成代码。
PatrickSchlüter'12

1
@tristopia:我见过人们编写宏,这些宏将代码块作为参数(我不一定推荐这样做)。例如:#define CHECK(call, onerr) if (0 != (call)) { onerr } else (void)0。它可以像一样使用CHECK(system("foo"), break;);,其中break;旨在指代包含CHECK()调用的循环。
理查德·汉森

16

虽然期望编译器可以优化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构造。


谢谢,因为逗号运算符不能保证执行顺序,所以这种嵌套是强制执行顺序的一种方法。
Marius

15
@Marius:错;逗号运算符是一个序列点,因此可以保证执行顺序。我怀疑您将其与函数参数列表中的逗号混淆了。
R .. GitHub STOP HELPING ICE

2
第二个充满异国情调的建议使我高兴。
Spidey 2013年

只是想补充一点,编译器被迫保留程序可观察的行为,因此优化do / while并没有多大意义(假设编译器优化正确)。
Marco A.

@MarcoA。尽管您是正确的,但我过去发现,编译器优化可以完全保留代码的功能,但是通过改变在单数上下文中似乎无能为力的行,将破坏多线程算法。举个例子Peterson's Algorithm
马吕斯(Marius)

11

詹斯·古斯特(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)如果在宏调用后省略分号,则会产生语法错误,否则将跳过下一个块。(实际上,这里没有“悬而未决”的问题,因为else绑定到最近的if,因此,这是宏中的那个)。

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


尽管非常聪明,但是这会导致编译器警告可能会悬挂其他对象,从而使它受到轰炸。
分段

1
通常,您使用宏来创建一个包含的环境,也就是说,您永远不会在宏中使用break(或continue)控制在外部开始/结束的循环,这只是不好的样式,并且隐藏了潜在的退出点。
mirabilos '16

Boost中还有一个预处理器库。令人惊讶的是什么?
Rene

这样做的风险else ((void)0)是有人可能正在写作,YOUR_MACRO(), f();并且该语法在语法上是有效的,但是永远不会调用f()。带有do while语法错误。
melpomene

@melpomene那么呢else do; while (0)
卡尔·雷

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循环中声明的局部变量。

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


通常的模式是在宏的变量名中添加下划线-例如int __i;
Blaisorblade 2011年

8
@Blaisorblade:其实这是不正确和非法的C;前导下划线保留供实现使用。您看到此“常规模式”的原因是由于读取了系统标​​头(“实现”),该标头必须将自己限制为该保留的名称空间。对于应用程序/库,您应该选择自己的晦涩,不太可能冲突的名称,且不带下划线,例如mylib_internal___i或类似名称。
R .. GitHub STOP HELPING ICE

2
@R.。您是对的-我实际上已经在Linux内核的“应用程序”中阅读了此内容,但是无论如何它都是一个例外,因为它不使用标准库(从技术上讲,是“独立的” C实现)一个“托管”的)。
Blaisorblade

3
@R ..不太正确:在所有情况下都保留前导下划线后跟大写或第二个下划线以供实现。在本地范围内不保留前导下划线和其他内容。
卢申科

@Leushenko:是的,但是区别非常微妙,我发现最好告诉人们根本不要使用这样的名字。大概了解这些细节的人已经知道我正在掩盖细节。:-)
R .. GitHub停止帮助ICE 2014年

6

说明

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

if (something)
  FOO(X); 

将扩展为:

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

并且g(X)将在if控制语句之外执行。使用do {} 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");

3
在宏中使用块钳位{}就足以捆绑宏代码,以便对相同的if条件路径执行所有操作。do-around用于在使用宏的位置强制使用分号。因此,强制执行宏具有更多功能。这包括使用时对尾部分号的要求。
亚历山大·斯托尔

2

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

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

将被翻译成

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

注意i++宏如何对其进行两次评估。这可能会导致一些有趣的错误。


15
这与do ... while(0)构造无关。
特伦特(Trent)

2
真正。但是与宏与函数以及如何编写充当函数的宏有关的话题……
John Nilsson

2
与上述类似,这不是答案,而是评论。关于主题:这就是为什么您只使用一次的原因:do { int macroname_i = (i); f(macroname_i); g(macroname_i); } while (/* CONSTCOND */ 0)
mirabilos
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.