提问人:jfm3 提问时间:10/1/2008 最后编辑:Lundinjfm3 更新时间:9/9/2023 访问量:115689
为什么要在宏中使用明显无意义的 do-while 和 if-else 语句?
Why use apparently meaningless do-while and if-else statements in macros?
问:
在许多 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)
答:
和在那里使它成为一个
宏后面的分号总是表示相同的意思。比方说你
有类似你的第二个宏的东西。do ... while
if ... else
#define BAR(X) f(x); g(x)
现在,如果你要在一个语句中使用,其中 if 语句的主体没有用大括号括起来,你会得到一个糟糕的惊喜。BAR(X);
if ... else
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();
如果你有一个更复杂的代码体需要放在它自己的块中,比如说声明局部变量,这是行不通的。在最一般的情况下,解决方案是使用 like 使宏成为单个语句,该语句采用分号而不会混淆。f(X)
do ... while
#define BAR(X) do { \
int i = f(X); \
if (i > 4) g(i); \
} while (0)
你不必使用 ,你也可以用它来做一些事情,尽管当在内部扩展时,它会导致“悬空的 else”,这可能会使现有的悬空 else 问题更难找到,如下面的代码所示。do ... while
if ... else
if ... else
if ... else
if (corge)
if (1) { f(corge); g(corge); } else;
else
gralt();
关键是在悬空分号错误的上下文中用完分号。当然,在这一点上可以(而且可能应该)争辩说,最好声明为实际函数,而不是宏。BAR
总之,可以解决 C 预处理器的缺点。当那些 C 风格指南告诉你放弃 C 预处理器时,这就是他们所担心的事情。do ... while
评论
#define BAR(X) (f(X), g(X))
if
if(1) {...} else void(0)
do {...} while(0)
for (int i = 0; i < max; ++i) { MYMACRO( SomeFunc(i)==true, {break;} ) }
MYMACRO
#define MYMACRO(X, CODE) do { if (X) { cout << #X << endl; {CODE}; } } while (0)
void(0)
(void)0
(void)0
if (cond) if (1) foo() else (void)0 else { /* dangling else body */ }
@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) ;
无论 的值如何,都将执行该函数。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.
}
评论
void doSomething() { int i = 25 ; { int i = x + 1 ; f(i) ; } ; // was MY_MACRO(32) ; }
x
MY_MACRO(i+7)
MY_MACRO(0x07 << 6)
\_\_LINE\_\_
__LINE__
inline
while(0)
if
#define FOO(x) { bar(x); }
while(0)
我不认为有人提到过,所以考虑一下
while(i<100)
FOO(i++);
将被翻译成
while(i<100)
do { f(i++); g(i++); } while (0)
请注意宏如何计算两次。这可能会导致一些有趣的错误。i++
评论
do { int macroname_i = (i); f(macroname_i); g(macroname_i); } while (/* CONSTCOND */ 0)
虽然编译器可以优化循环,但还有另一种解决方案不需要这种结构。解决方案是使用逗号运算符: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 结构。
评论
Peterson's Algorithm
由于某些原因,我无法对第一个答案发表评论......
你们中的一些人展示了带有局部变量的宏,但没有人提到你不能在宏中使用任何名称!总有一天它会咬用户!为什么?因为输入参数已替换到宏模板中。在您的宏示例中,您使用了可能是最常用的变量名称 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 循环。
因此,切勿在宏中使用公共变量名称!
评论
int __i;
mylib_internal___i
上面的答案解释了这些结构的含义,但两者之间存在未提及的显着差异。事实上,比起构造,更喜欢 是有原因的。do ... while
if ... else
该结构的问题在于它不会强迫您输入分号。就像在这段代码中一样:if ... else
FOO(1)
printf("abc");
虽然我们错误地省略了分号,但代码将扩展到
if (1) { f(X); g(X); } else
printf("abc");
并且会静默编译(尽管某些编译器可能会发出无法访问代码的警告)。但是该语句永远不会被执行。printf
do ... while
construct 没有这样的问题,因为 之后唯一有效的标记是分号。while(0)
评论
FOO(1),x++;
do ... while
do ... while (0)
break
continue
do ... while (0)
if
break
continue
do {...} while(0)
do { ... } while(0)
if whatever
do {...} while(0)
if
#define CHECK(call, onerr) if (0 != (call)) { onerr } else (void)0
CHECK(system("foo"), break;);
break;
CHECK()
解释
do {} while (0)
并确保宏扩展到仅 1 条指令。否则:if (1) {} else
if (something)
FOO(X);
将扩展到:
if (something)
f(X); g(X);
并且将在控制语句之外执行。使用 和 时可以避免这种情况。g(X)
if
do {} while (0)
if (1) {} else
更好的选择
使用 GNU 语句表达式(不是标准 C 的一部分),您有更好的方法来解决此问题,只需使用:do {} 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)
break
continue
((void)0)
else
if
如果您对使用 C 预处理器可以或多或少安全地完成各种事情感兴趣,请查看该库。
评论
break
continue
else ((void)0)
YOUR_MACRO(), f();
f()
do
while
else do; while (0)
评论
void
do while
if (1) { ... } else ((void)0)