为什么在macros中使用明显无意义的do-while和if-else语句?
在许多C / C ++macros中,我看到macros包装的代码看起来像是一个无意义的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)
这样do ... while
, if ... else
在那里,使你的macros后面的分号总是意味着同样的事情。 假设你有第二个macros这样的东西。
#define BAR(X) f(x); g(x)
现在如果你要使用BAR(X);
在if ... else
语句中, if ... else
语句的主体不包含在大括号中,那么您会得到一个不好的惊喜。
if (corge) BAR(corge); else gralt();
上面的代码将扩展到
if (corge) f(corge); g(corge); else gralt();
这在语法上是不正确的,因为else不再与if关联。 在macros中用大括号括起来是没有用的,因为大括号后面的分号在语法上是不正确的。
if (corge) {f(corge); g(corge);}; else gralt();
有两种方法可以解决这个问题。 首先是使用逗号来排列macros中的语句,而不会使它像expression式那样动作。
#define BAR(X) f(X), g(X)
上面的bar BAR
版本将上面的代码扩展到后面的代码中,这在语法上是正确的。
if (corge) f(corge), g(corge); else gralt();
这不起作用,而不是f(X)
你有一个更复杂的代码体,需要在自己的块,例如声明本地variables。 在最一般的情况下,解决scheme是使用类似do ... while
这样的macros来使macros成为一个分号而不会混淆的单个语句。
#define BAR(X) do { \ int i = f(X); \ if (i > 4) g(i); \ } while (0)
你不必使用do ... while
,你可以用if ... else
来做一些事情,虽然if ... else
在if ... else
内部扩展时会导致一个“ 其他 “,这可能会使现有的悬而未决的问题更难以find,如下面的代码。
if (corge) if (1) { f(corge); g(corge); } else; else gralt();
重点是在悬挂分号错误的上下文中用完分号。 当然,它可能(也可能应该)在这一点上争辩说,把BAR
作为一个实际的function而不是macros来宣称是更好的。
总之, do ... while
是为了解决C预处理器的缺点。 当那些C风格的指南告诉你去掉C预处理器时,这就是他们所担心的那种事情。
macros将复制/粘贴的文本预处理器放在真正的代码中; macros的作者希望replace会产生有效的代码。
有三个好的“技巧”可以成功:
帮助macros的行为像真正的代码
普通代码通常以分号结尾。 如果用户查看代码不需要一个…
doSomething(1) ; DO_SOMETHING_ELSE(2) // <== Hey? What's this? doSomethingElseAgain(3) ;
这意味着如果分号不存在,用户希望编译器产生一个错误。
但真正的真正的好理由是,在某个时候,macros的作者可能需要用一个真正的函数(也许是内联的)来replacemacros。 所以macros应该像一个一样。
所以我们应该有一个需要分号的macros。
生成有效的代码
如jfm3的答案所示,macros有时包含多条指令。 如果macros在if语句中使用,这将是有问题的:
if(bIsOk) MY_MACRO(42) ;
这个macros可以扩展为:
#define MY_MACRO(x) f(x) ; g(x) if(bIsOk) f(42) ; g(42) ; // was MY_MACRO(42) ;
g
函数将被执行,而不pipebIsOk
的值bIsOk
。
这意味着我们必须为macros添加一个范围:
#define MY_MACRO(x) { f(x) ; g(x) ; } if(bIsOk) { f(42) ; g(42) ; } ; // was MY_MACRO(42) ;
生成有效的代码2
如果这个macros是这样的:
#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) ; }
当然,这段代码不会编译。 所以,解决scheme再次使用一个范围:
#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可以创build一个范围,从而封装macros的代码,最后需要一个分号,从而扩展成需要的代码。
奖金?
C ++编译器会优化掉do / while循环,因为它的后置条件是错误的情况在编译时是已知的。 这意味着一个macros如:
#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 – 你对这个问题有一个很好的答案。 你可能还想补充说,macros语法也可以防止可能更危险(因为没有错误)与简单的'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
结构的问题在于它不会强制你input分号。 像这样的代码:
FOO(1) printf("abc");
虽然我们遗漏了分号(错误),但代码会扩展到
if (1) { f(X); g(X); } else printf("abc");
并会静静地编译(尽pipe一些编译器可能会发出一个不可达代码的警告)。 但是printf
语句永远不会被执行。
do ... while
构造不存在这样的问题,因为while(0)
之后的唯一有效标记是分号。
虽然期望编译器优化do { ... } while(false);
循环,还有另外一个解决scheme,不需要那个构造。 解决方法是使用逗号运算符:
#define FOO(X) (f(X),g(X))
或者甚至更加异乎寻常地:
#define FOO(X) g((f(X),(X)))
虽然这将适用于单独的说明,但它不适用于构buildvariables并将其用作#define
一部分的情况:
#define FOO(X) (int s=5,f((X)+s),g((X)+s))
有了这个将被迫使用do / while结构。
Jens Gustt的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
在给定块内continue
工作,但((void)0)
创build语法错误,如果分号在macros调用,否则将跳过下一个块。 (这里实际上并不存在“悬而未决”的问题,因为else
绑定到最近的if
,这是macros中的那个)。
如果您对使用C预处理器可以或多或less安全地完成某些事情感兴趣,请检查该库。
由于某些原因,我不能评论第一个答案…
你们中的一些人用局部variables显示了macros,但没有人提到你不能在macros中使用任何名字! 它会咬有用的一天! 为什么? 因为input参数被replace到您的macros模板中。 而在你的macros例子中,你已经使用了可能最常用的variables名i 。
例如,当下面的macros
#define FOO(X) do { int i; for (i = 0; i < (X); ++i) do_something(i); } while (0)
用于以下function
void some_func(void) { int i; for (i = 0; i < 10; ++i) FOO(i); }
macros将不会使用在some_func开头声明的预期variablesi,而是在macros的do … while循环中声明的局部variables。
因此,千万不要在macros中使用通用variables名!
我不认为这是考虑到这一点
while(i<100) FOO(i++);
将被翻译成
while(i<100) do { f(i++); g(i++); } while (0)
注意i++
是如何被macros评估两次的。 这可能会导致一些有趣的错误。
do {} while (0)
和if (1) {} else
,可以简单地使用({})
:
#define FOO(X) ({f(X); g(X);})
而且这个语法与返回值兼容( do {} while (0)
不是),如下所示:
return FOO("X");
我发现这个技巧非常有用,在你必须顺序处理特定值的情况下。 在每个处理级别,如果出现一些错误或无效的情况,可以避免进一步的处理和提前爆发。 例如
#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);