在许多C/ c++宏中,我看到宏的代码被包装在似乎毫无意义的do while循环中。下面是一些例子。
#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)
在许多C/ c++宏中,我看到宏的代码被包装在似乎毫无意义的do while循环中。下面是一些例子。
#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)
做…如果……还有别的方法可以让 宏后面的分号总是同样的意思。比如说你 就像你的第二个宏。
#define BAR(X) f(x); g(x)
如果你用BAR(X);如果…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…而使宏成为一个单独的语句,使用分号而不会造成混淆。
#define BAR(X) do { \
int i = f(X); \
if (i > 4) g(i); \
} while (0)
你不必用do…不过,你可以用if…Else也是,尽管when if…Else扩展到if…否则它会导致“悬空Else”,这可能会使现有的悬空Else问题更难找到,如下面的代码所示。
if (corge)
if (1) { f(corge); g(corge); } else;
else
gralt();
重点是在悬空分号错误的上下文中使用分号。当然,在这一点上,可以(也可能应该)认为将BAR声明为一个实际的函数而不是宏会更好。
总之,做…而有工作围绕C预处理器的缺点。当那些C风格指南告诉你不要使用C预处理器时,这就是他们所担心的事情。
@jfm3 -你对这个问题的回答很好。你可能还想补充一点,宏习语还可以通过简单的'if'语句防止可能更危险的(因为没有错误)意外行为:
#define FOO(x) f(x); g(x)
if (test) FOO( baz);
扩展:
if (test) f(baz); g(baz);
这在语法上是正确的,因此没有编译器错误,但可能会产生意想不到的后果,即总是会调用g()。
宏是预处理器将放入真正代码中的文本片段的复制/粘贴;宏的作者希望替换将产生有效的代码。
这里有三个成功的“秘诀”:
帮助宏像真正的代码一样运行
普通代码通常以分号结束。如果用户视图代码不需要一个…
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.
}
我想没有提到过,所以考虑一下
while(i<100)
FOO(i++);
会被翻译成
while(i<100)
do { f(i++); g(i++); } while (0)
注意i++是如何被宏求值两次的。这可能会导致一些有趣的错误。
虽然编译器应该优化掉do{…},(假);循环,还有另一个解决方案不需要这个结构。解决方案是使用逗号操作符:
#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结构。
由于某些原因,我不能评论第一个答案……
有些人展示了带局部变量的宏,但没有人提到不能在宏中使用任何名称!它总有一天会伤害用户!为什么?因为输入参数被替换到宏模板中。在你的宏例子中,你使用了最常用的变量名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循环。
因此,永远不要在宏中使用通用变量名!
上述答案解释了这些结构的含义,但两者之间有一个未被提及的显著差异。事实上,有一个理由更喜欢做…而如果…其他结构。
如果……Else结构的特点是它不强制你输入分号。就像下面的代码:
FOO(1)
printf("abc");
尽管我们(错误地)漏掉了分号,但代码将扩展为
if (1) { f(X); g(X); } else
printf("abc");
并且将静默地编译(尽管一些编译器可能会对不可访问的代码发出警告)。但是printf语句永远不会被执行。
做……While构造就没有这样的问题,因为While(0)后面唯一有效的标记是分号。
解释
执行{}while(0)和if (1) {} else确保宏被扩展到只有一条指令。否则:
if (something)
FOO(X);
将扩大为:
if (something)
f(X); g(X);
g(X)将在if控制语句之外执行。当使用do {} while(0)和if (1) {} else时,可以避免这种情况。
更好的选择
使用GNU语句表达式(不是标准C的一部分),你有一个比{}while(0)和if (1) {} else更好的方法来解决这个问题,只需使用({}):
#define FOO(X) ({f(X); g(X);})
并且此语法与返回值兼容(注意do {} while(0)不兼容),例如:
return FOO("X");
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) construct, break和continue仍然在给定的块内工作,但是((void)0)如果在宏调用后省略了分号,将会创建一个语法错误,否则将跳过下一个块。(这里实际上不存在“悬空else”问题,因为else绑定到最近的if,也就是宏中的if。)
如果您对C预处理器可以或多或少安全地完成的事情感兴趣,请查看该库。