在许多C / C ++宏中,我看到宏的代码包含在看似无意义的做的循环中。以下是一些例子。

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

我无法看到正在做什么。为什么不在没有它的情况下写这个呢?

#define FOO(X) f(X); g(X)
有帮助吗?

解决方案

do ... while if ... else 是为了让它成为一个 你的宏之后的分号总是意味着同样的事情。我们说你 有像你的第二个宏。

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

现在,如果你在 if ... else 语句中使用 BAR(X); ,那么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 内扩展如果...... else 它会导致“悬挂其他",这可能会使现有悬挂的其他问题更难找到,如下面的代码所示。

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

重点是在悬挂分号错误的情况下用掉分号。当然,它可以(也可能应该)在这一点上争论说,将 BAR 声明为实际函数而不是宏是更好。

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

其他提示

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

有三个好的“提示”要成功:

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

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

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

无论 bIsOk 的值如何,都将执行 g 函数。

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

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

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

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

if (test) FOO( baz);

扩展为:

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

这在语法上是正确的,因此没有编译器错误,但可能出乎意料的结果是g()将始终被调用。

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

if ... else 构造的问题是它不会强制你输出分号。就像在这段代码中一样:

FOO(1)
printf("abc");

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

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

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

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

虽然预计编译器会优化 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结构。

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 绑定到最近的 if ,这是宏中的那个。)

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

出于某些原因,我无法评论第一个答案......

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

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

解释

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 {}而(0)不是),如:

return FOO("X");

我认为没有提到它,所以请考虑这个

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

将被翻译成

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

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

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

#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);
许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top