关于c ++:为什么在宏中使用明显无意义的do-while和if-else语句?

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

在许多C/C++宏中,我看到宏的代码被封装在一个似乎没有意义的EDCOX1×0循环中。下面是一些例子。

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

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

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


do ... whileif ... else在那里使它成为宏后的分号始终表示相同的内容。让我们说你有点像你的第二个宏。

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

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

1
2
3
4
if (corge)
  BAR(corge);
else
  gralt();

上述代码将扩展为

1
2
3
4
if (corge)
  f(corge); g(corge);
else
  gralt();

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

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

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

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

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

1
2
3
4
if (corge)
  f(corge), g(corge);
else
  gralt();

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

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

你不必使用do ... while,你也可以用if ... else做一些东西,尽管当if ... elseif ... else的内部扩展时,它会导致一个"悬而未决的其他"问题,这可能使现有的悬而未决的其他问题更难找到,如下面的代码所示。

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

重点是在悬空分号错误的情况下使用分号。当然,在这一点上,我们可以(也可能应该)提出这样的观点:最好将BAR声明为实际函数,而不是宏。

总之,do ... while是用来解决C预处理器的缺点的。当那些C样式的指南告诉您停止使用C预处理器时,这是他们担心的事情。


宏是复制/粘贴的文本,预处理程序会将其放入真正的代码中;宏的作者希望替换后的代码能产生有效的代码。

成功的秘诀有三个:

帮助宏像真正的代码一样工作

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

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

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

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

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

生成有效代码

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

1
2
if(bIsOk)
   MY_MACRO(42) ;

此宏可以扩展为:

1
2
3
4
#define MY_MACRO(x) f(x) ; g(x)

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

无论bIsOk的值如何,都将执行g功能。

这意味着我们必须向宏添加一个作用域:

1
2
3
4
#define MY_MACRO(x) { f(x) ; g(x) ; }

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

生成有效代码2

如果宏类似于:

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

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

1
2
3
4
5
void doSomething()
{
    int i = 25 ;
    MY_MACRO(32) ;
}

因为它将扩展为:

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

当然,这段代码不会编译。因此,解决方案再次使用了一个范围:

1
2
3
4
5
6
7
#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循环:

1
2
3
4
5
do
{
    // code
}
while(false) ;

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

奖金?

C++编译器将优化Do/while循环,因为它的后置条件为false的事实在编译时是已知的。这意味着宏类似于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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.
}

将正确展开为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.
}

然后编译并优化为

1
2
3
4
5
6
7
8
9
10
11
void doSomething(bool bIsOk)
{
   int i = 25 ;

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

   // Etc.
}


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

1
2
3
#define FOO(x)  f(x); g(x)

if (test) FOO( baz);

扩展到:

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

它在语法上是正确的,因此不存在编译器错误,但可能会导致意外的结果,即始终调用g()。


上述答案解释了这些结构的含义,但未提及的两个结构之间存在显著差异。事实上,有理由选择do ... while而不是if ... else结构。

if ... else构造的问题是它不会强制您放置分号。就像这段代码:

1
2
FOO(1)
printf("abc");

尽管我们遗漏了分号(错误地),但代码将扩展到

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

并且将静默编译(尽管有些编译器可能对无法访问的代码发出警告)。但printf声明永远不会执行。

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


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

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

或者更离奇地说:

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

虽然这在单独的指令中可以很好地工作,但在构造变量并将其用作#define的一部分的情况下,它将无法工作:

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

使用此方法,将强制使用do/while构造。


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

1
2
3
#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绑定,后者是宏中的if。)

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


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

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

例如,当以下宏

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

在以下函数中使用

1
2
3
4
5
void some_func(void) {
    int i;
    for (i = 0; i < 10; ++i)
        FOO(i);
}

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

因此,不要在宏中使用通用变量名!


解释

do {} while (0)if (1) {} else是为了确保宏只扩展到1条指令。否则:

1
2
if (something)
  FOO(X);

将扩展到:

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

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

更好的选择

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

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

该语法与返回值(注意,do {} while (0)不是)兼容,如:

1
return FOO("X");


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

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

会被翻译成

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

注意如何用宏对i++进行两次计算。这会导致一些有趣的错误。


我发现这个技巧非常有用,在您必须按顺序处理特定值的情况下。在每一个处理级别,如果发生了一些错误或无效的情况,您可以避免进一步的处理并提前中断。例如

1
2
3
4
5
6
7
#define CALL_AND_RETURN(x)  if ( x() == false) break;
do {
     CALL_AND_RETURN(process_first);
     CALL_AND_RETURN(process_second);
     CALL_AND_RETURN(process_third);
     //(simply add other calls here)
} while (0);


if (1) {}上使用do {} while (0)的原因是,在将宏do {} while (0)调用为其他类型的块之前,没有任何方法可以更改代码。例如,如果调用一个由if (1) {}包围的宏,例如:

1
2
else
  MACRO(x);

这实际上是一个else if。细微差别