为什么在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 ... whileif ... 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 ... elseif ... 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)构造不同, breakcontinue在给定块内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);