当前位置:  开发笔记 > 编程语言 > 正文

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

如何解决《为什么在宏中使用明显无意义的do-while和if-else语句?》经验,为你挑选了7个好方法。

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

jfm3.. 797

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预处理器时,这就是他们担心的事情.



1> jfm3..:

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预处理器时,这就是他们担心的事情.


在学习了所有这些之后,为什么我作为程序员感觉更糟...
@DawidFerenczy:尽管你和我在四年半前都有一个好点,但我们必须生活在现实世界中.除非我们能够保证代码中的所有`if`语句等都使用大括号,否则这样包装宏是一种避免问题的简单方法.
在if,while和for语句中总是使用大括号,这不是一个强有力的论据吗?如果您总是这样做(例如,MISRA-C所要求的那样),上述问题就会消失.
逗号示例应该是`#define BAR(X)(f(X),g(X))`否则运算符优先级可能会弄乱语义.
注意:`if(1){...} else void(0)`形式比`do {...} while(0)`更安全,因为宏的参数是包含在宏扩展中的代码,因为它不会改变break或continue关键字的行为.例如:`for(int i = 0; i @ace`void(0)`是一个错字,我的意思是`(void)0`.而且我相信这**解决了"悬空的"问题:注意`(void)0`之后没有分号.在这种情况下悬空的其他东西(例如`if(cond)if(1)foo()else(void)0 else {/*dangling else body*/}`)会触发编译错误.这是一个[展示它的实例](http://ideone.com/2vJa7E)
但是为什么不简单地#define FOO(X){f(X); g(X);}?

2> paercebal..:

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

有三个好的"提示"可以成功:

帮助宏的行为像真正的代码

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

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


注意,改变宏内联函数改变一些标准的预定义的宏,例如下面的代码显示在__FUNCTION__和__LINE__的变化的:#include 中的#define Fmacro()的printf( "%S%d \n" 个,__FUNCTION__, __LINE__)内嵌无效鳍线(){printf的( "%S%d \n" 个,__FUNCTION__,__LINE__); } int main(){Fmacro(); 鳍线(); 返回0; (粗体术语应该用双下划线括起来 - 格式化程序不好!)
这个答案有一些轻微但不完全无关紧要的问题.例如:`void doSomething(){int i = 25; {int i = x + 1; f(i); }; //是MY_MACRO(32); }`不是正确的扩展; 扩展中的`x`应该是32.更复杂的问题是`MY_MACRO(i + 7)`的扩展是什么.另一个是`MY_MACRO(0x07 << 6)`的扩展.有很多很好的东西,但是有些东西是我的,而且没有交叉.

3> Michael Burr..:

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

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

if (test) FOO( baz);

扩展为:

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

这在语法上是正确的,所以没有编译器错误,但可能有意外的结果,g()将始终被调用.


"可能是无意的"?我会说"当然是无意的",否则程序员需要被拿出并开枪(而不是用鞭子严厉惩罚).
或者他们可能会获得加薪,如果他们正在为一个三字母代理机构工作并偷偷地将该代码插入广泛使用的开源程序...... :-)
这条评论让我想起了最近在Apple操作系统中发现的SSL证书验证错误中的goto失败行

4> ybungalobill..:

上述答案解释了这些结构的含义,但两者之间存在显着差异,未提及.其实,还有一个原因,更喜欢do ... whileif ... else结构.

if ... else构造的问题是它不会强迫你输出分号.喜欢这段代码:

FOO(1)
printf("abc");

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

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

并将静默编译(虽然一些编译器可能会发出无法访问代码的警告).但该printf声明永远不会被执行.

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


使用`do {...} while(0)`而不是`if whatever`构造的另一个原因是它的惯用性.`do {...} while(0)`结构很普遍,众所周知并且被许多程序员广泛使用.它的基本原理和文件很容易知道.对于`if`结构不是这样.因此,在进行代码审查时需要花费较少的精力.
@RichardHansen:还是不太好,因为从宏观调用的角度来看,你不知道它是扩展为语句还是表达式.如果有人假设后者,她可能会写"FOO(1),x ++;`这将再次给我们一个误报.只需使用`do ... while`即可.
我没有看到你在哪里可以放置一个`break`或`continue`,它将被视为你的宏`do {...} while(0)`pseudo-loop.即使在宏参数中,它也会产生语法错误.

5> Marius..:

虽然预计编译器会优化掉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:错; 逗号运算符是一个序列点,因此**确保执行顺序.我怀疑你把它与函数参数列表中的逗号混淆了.
第二个充满异国情调的建议使我高兴。

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)构造不同,break并且continue仍然在给定块内部工作,但是((void)0)如果在宏调用之后省略分号则会产生语法错误,否则将跳过下一个块.(这里实际上没有"悬空的"问题,因为它else绑定到最近的if,即宏中的那个.)

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



7> 小智..:

由于某些原因,我不能评论第一个答案......

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

因此,永远不要在宏中使用常见的变量名!


@Blaisorblade:其实这是不正确的非法C; 前导下划线保留供实现使用.您看到这种"通常模式"的原因是从读取系统头("实现"),它必须将自己限制在这个保留的命名空间.对于应用程序/库,您应该选择自己的模糊,不可能碰撞的名称而不用下划线,例如`mylib_internal ___ i`或类似名称.
@R ..这不太正确:前导下划线*后跟大写或第二下划线*保留用于所有上下文中的实现.在本地范围内不保留前导下划线,后跟其他内容.
@R ..你是对的 - 我实际上是在一个''应用程序''中读到这个,Linux内核,但它是一个例外,因为它不使用标准库(技术上,''独立''C实现代替一个''托管''一个).
推荐阅读
U友50081205_653
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有