在许多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)
答案 0 :(得分:770)
do ... while
和if ... 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
时会导致“dangling else”,这可能会使现有悬挂的其他问题更难找到,如下面的代码所示。
if (corge)
if (1) { f(corge); g(corge); } else;
else
gralt();
重点是在悬挂分号错误的情况下用掉分号。当然,在这一点上可以(并且可能应该)讨论将BAR
声明为实际函数而不是宏来更好。
总之,do ... while
可以解决C预处理器的缺点。当那些C风格指南告诉你裁掉C预处理器时,这是他们担心的事情。
答案 1 :(得分:143)
宏是预处理器将放入正版代码的复制/粘贴文本;宏的作者希望替换产生有效的代码。
有三个好的“提示”可以成功:
正常代码通常以分号结束。如果用户查看不需要的代码......
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) ;
如果宏类似于:
#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.
}
答案 2 :(得分:51)
@ jfm3 - 你对这个问题有一个很好的答案。您可能还想补充一点,宏语法也可以通过简单的“if”语句防止可能更危险(因为没有错误)的意外行为:
#define FOO(x) f(x); g(x)
if (test) FOO( baz);
扩展为:
if (test) f(baz); g(baz);
这在语法上是正确的,因此没有编译器错误,但可能出乎意料的结果是g()将始终被调用。
答案 3 :(得分:23)
上述答案解释了这些结构的含义,但两者之间存在显着差异,未提及。事实上,有理由更喜欢do ... while
到if ... else
构造。
if ... else
构造的问题在于它不会强制你输出分号。就像在这段代码中一样:
FOO(1)
printf("abc");
虽然我们遗漏了分号(错误地),但代码将扩展为
if (1) { f(X); g(X); } else
printf("abc");
并将静默编译(尽管某些编译器可能会发出无法访问代码的警告)。但printf
语句永远不会被执行。
do ... while
构造没有这样的问题,因为while(0)
之后唯一有效的标记是分号。
答案 4 :(得分:16)
虽然预计编译器会优化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结构。
答案 5 :(得分:10)
Jens Gustedt的P99 preprocessor library(是的,事实上这样的事情也引起了我的注意!)通过定义以下内容,以一种小但重要的方式改进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)
会产生语法错误,如果宏调用后省略分号,否则会跳过下一个块。 (这里实际上没有&#34;悬挂其他&#34;问题,因为else
绑定到最近的if
,这是宏中的那个。)
如果您对使用C预处理器可以或多或少安全地完成的各种事情感兴趣,请查看该库。
答案 6 :(得分:6)
出于某些原因,我无法评论第一个答案......
有些人展示了带有局部变量的宏,但是没有人提到你不能只在宏中使用任何名字!有一天它会咬住用户!为什么?因为输入参数被替换为宏模板。在您的宏示例中,您使用了可能最常用的变量名称 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循环中声明的局部变量。
因此,永远不要在宏中使用公共变量名!
答案 7 :(得分:3)
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 statement expression(不是标准C的一部分),只需使用do {} while (0)
,您就可以比if (1) {} else
和({})
更好地解决此问题:
#define FOO(X) ({f(X); g(X);})
此语法与返回值兼容(请注意do {} while (0)
isn&t; t),如:
return FOO("X");
答案 8 :(得分:2)
我认为没有提到它,所以请考虑这个
while(i<100)
FOO(i++);
将被翻译成
while(i<100)
do { f(i++); g(i++); } while (0)
注意宏如何评估i++
两次。这可能会导致一些有趣的错误。
答案 9 :(得分:2)
我发现这个技巧非常有用,在必须按顺序处理特定值的情况下。在每个处理级别,如果发生某些错误或无效条件,您可以避免进一步处理并提前中断。 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);
答案 10 :(得分:0)
在do {} while (0)
上使用if (1) {}
的原因是,在调用宏do {} while (0)
成为某种其他类型的块之前,没有人可以更改代码。例如,如果您调用由if (1) {}
包围的宏,例如:
else
MACRO(x);
实际上是else if
。细微差别