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


评论

对于else的示例,我将在末尾添加一个void类型的表达式...就像((void)0)。

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

#1 楼

使用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 ... elseif ... else内扩展时会导致“悬空”,这可能使现有悬空的其他问题更难找到,如以下代码所示。

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


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

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

评论


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

– Steve Melnikoff
09年4月2日在22:51

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

– Stewart
2011年5月19日在12:58

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

– Steve Melnikoff
13年20月20日在17:16

注意:对于参数为宏扩展中包含的代码的宏,if(1){...} else void(0)格式比do {...} while(0)格式更安全,因为它不会t更改break或continue关键字的行为。例如:for(int i = 0; i
–克里斯·克莱恩(Chris Kline)
2015年6月22日在16:23



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

–克里斯·克莱恩(Chris Kline)
15年7月27日在11:07



#2 楼

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

有三个成功的“技巧”可以成功实现:

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

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

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


评论


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

–古努比
2012年8月23日在10:52



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

–乔纳森·莱弗勒(Jonathan Leffler)
13年8月26日在4:17

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

–斯科特
17年8月22日23:07



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

–安德鲁(Andrew)
19年1月3日在6:01

#3 楼

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

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

if (test) FOO( baz);


扩展为:

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


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

评论


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

–劳伦斯·多尔(Lawrence Dol)
2010-4-22的2:45

或者,如果他们正在一家三字母代理机构工作,并且秘密地​​将该代码插入到广泛使用的开源程序中,那么他们可能会得到加薪... :-)

–R .. GitHub停止帮助ICE
2012年4月26日下午3:09

这个评论让我想起了苹果操作系统中最近的SSL证书验证错误中的goto失败行。

– Gerard Sexton
2014年3月11日14:31

#4 楼

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

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

FOO(1)
printf("abc");


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

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


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

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

评论


@RichardHansen:仍然不是很好,因为从宏调用的角度来看,您不知道它是扩展为语句还是表达式。如果有人假设以后再写,她可能会写FOO(1),x ++;这将再次给我们带来误报。只需使用do ...就可以了。

–亚科夫·加尔卡(Yakov Galka)
2012年8月3日17:35



记录宏以避免误解就足够了。我确实同意do ... while(0)是更好的选择,但它有一个缺点:中断或继续将控制do ... while(0)循环,而不是包含宏调用的循环。因此,如果技巧仍然有价值。

–理查德·汉森(Richard Hansen)
2012年8月3日在21:10

我看不到您可以在宏的内部放置中断或继续的地方进行{...} while(0)伪循环。即使在宏参数中,也会产生语法错误。

–帕特里克·施吕特(PatrickSchlüter)
2012年12月21日在16:02

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

–帕特里克·施吕特(PatrickSchlüter)
2012年12月21日在16:05

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

–理查德·汉森(Richard Hansen)
13年7月18日在17:18



#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构造。

评论


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

–马里乌斯(Marius)
2011-6-5 13:39

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

–R .. GitHub停止帮助ICE
2012年12月12日下午5:02

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

–蜘蛛
2013年6月20日19:08

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

– Marco A.
15年3月31日在9:42

@MarcoA。虽然您是正确的,但我过去发现编译器优化可以完全保留代码的功能,但是通过更改在单数上下文中似乎无能为力的行,将破坏多线程算法。彼得森算法的例子。

–马里乌斯(Marius)
16年7月25日在12:29

#6 楼

詹斯·古斯特(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预处理器或多或少地安全地完成操作,请查看该库。

评论


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

–细分
15年3月26日在19:20

通常,您使用宏来创建一个封闭的环境,也就是说,您永远不要在宏内部使用换行(或继续)来控制从外部开始/结束的循环,这只是一种不好的样式,而且会隐藏潜在的退出点。

– mirabilos
16年12月21日在11:51

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

–雷恩
17年6月1日在8:45

else((void)0)的风险是有人可能正在写YOUR_MACRO(),f();而且它在语法上是有效的,但是永远不要调用f()。使用do会导致语法错误。

–melpomene
19年6月7日在20:42

@melpomene那么其他呢?而(0)?

–卡尔雷
19年8月15日在2:29

#7 楼

说明
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");


评论


在宏中使用block-clamping {}就足以捆绑宏代码,以便对同一if条件路径执行所有操作。 do-while操作用于在使用宏的地方强制使用分号。因此,强制执行宏具有更多功能。这包括使用时对尾部分号的要求。

–亚历山大·斯托尔
18/12/21在14:38



#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;。

–布莱布莱德
2011年12月22日在1:03

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

–R .. GitHub停止帮助ICE
2012年4月12日下午5:05

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

–布莱布莱德
2012年4月12日在16:23

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

–卢申科
2014年5月3日,3:15

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

–R .. GitHub停止帮助ICE
2014年5月3日,3:16



#9 楼

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

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


将被翻译成

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


通知i++如何被宏两次评估。这可能会导致一些有趣的错误。

评论


这与do ... while(0)构造无关。

–特伦特
08年11月18日在19:54

真正。但是与宏与函数以及如何编写充当函数的宏有关...

–约翰·尼尔森(John Nilsson)
08年11月26日在19:21

与上述类似,这不是答案,而是评论。主题:这就是为什么您只使用一次的原因:do {int macroname_i =(i); f(宏名_i); g(宏名_i); } while(/ * CONSTCOND * / 0)

– mirabilos
16年12月21日在11:49