在许多 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)
void
类型的表达式...例如 ((void)0)。
do while
构造与 return 语句不兼容,因此 if (1) { ... } else ((void)0)
构造在标准 C 中具有更兼容的用法。在 GNU C 中,您会更喜欢我的回答中描述的构造。
do ... while
和 if ... else
是为了让宏后面的分号始终表示相同的意思。假设您有类似第二个宏的东西。
#define BAR(X) f(x); g(x)
现在,如果您在 if ... else
语句中使用 BAR(X);
,其中 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”,这可能会使现有的悬空 else 问题更难找到,如下面的代码所示。
if (corge)
if (1) { f(corge); g(corge); } else;
else
gralt();
关键是在悬挂分号错误的情况下用完分号。当然,在这一点上可以(并且可能应该)争辩说,将 BAR
声明为实际函数而不是宏会更好。
总之,do ... while
可以解决 C 预处理器的缺点。当那些 C 风格指南告诉你放弃 C 预处理器时,这就是他们担心的事情。
宏是预处理器将放入正版代码中的文本的复制/粘贴片段;宏的作者希望替换将产生有效的代码。
成功的三个好“技巧”:
帮助宏表现得像真正的代码
普通代码通常以分号结尾。如果用户查看代码不需要...
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 循环,因为它的后置条件为假这一事实在编译时是已知的。这意味着像这样的宏:
#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
应该是 32。更复杂的问题是 MY_MACRO(i+7)
的展开式是什么。另一个是MY_MACRO(0x07 << 6)
的展开。有很多好的,但也有一些不带点的 i 和不交叉的 t。
\_\_LINE\_\_
它会呈现为 __LINE__。恕我直言,最好只对代码使用代码格式;例如,__LINE__
(不需要任何特殊处理)。 PS我不知道这在2012年是否属实;从那时起,他们对引擎进行了相当多的改进。
inline
函数(标准允许)
@jfm3 - 你对这个问题有一个很好的答案。您可能还想补充一点,宏成语还可以通过简单的“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
构造的问题在于它不会强制您输入分号。就像在这段代码中:
FOO(1)
printf("abc");
虽然我们省略了分号(错误地),但代码将扩展为
if (1) { f(X); g(X); } else
printf("abc");
并将静默编译(尽管某些编译器可能会针对无法访问的代码发出警告)。但是 printf
语句永远不会被执行。
do ... while
构造没有这样的问题,因为在 while(0)
之后的唯一有效标记是分号。
FOO(1),x++;
这将再次给我们一个误报。只需使用 do ... while
即可。
do ... while (0)
更可取,但它有一个缺点:break
或 continue
将控制 do ... while (0)
循环,而不是包含宏调用的循环。所以 if
技巧仍然有价值。
do {...} while(0)
伪循环内的 break
或 continue
放在哪里。即使在宏参数中也会产生语法错误。
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)
和 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)
不兼容),如下所示:
return FOO("X");
虽然预计编译器会优化掉 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
点的例子。
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)
会产生语法错误,否则会跳过下一个块。 (这里实际上没有“悬空的 else”问题,因为 else
绑定到最近的 if
,它是宏中的那个。)
如果您对可以使用 C 预处理器或多或少安全地完成的各种事情感兴趣,请查看该库。
break
(或 continue
)来控制在外部开始/结束的循环,这只是一种糟糕的风格并隐藏了潜在的退出点。
else ((void)0)
的风险在于有人可能正在编写 YOUR_MACRO(), f();
,它在语法上是有效的,但永远不要调用 f()
。 do
while
是语法错误。
else do; while (0)
呢?
由于某些原因,我无法对第一个答案发表评论......
你们中的一些人展示了带有局部变量的宏,但没有人提到你不能只在宏中使用任何名称!总有一天它会咬用户!为什么?因为输入参数被替换到您的宏模板中。在您的宏示例中,您使用了可能最常用的变量名称 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
或类似名称。
我不认为它被提及所以考虑这个
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)
#define BAR(X) (f(X), g(X))
否则运算符优先级可能会混淆语义。if
语句等都使用大括号,否则像这样包装宏是避免问题的简单方法。if(1) {...} else void(0)
形式比do {...} while(0)
更安全,因为它不会改变 break 或 continue 关键字的行为。例如:当MYMACRO
定义为#define MYMACRO(X, CODE) do { if (X) { cout << #X << endl; {CODE}; } } while (0)
时,for (int i = 0; i < max; ++i) { MYMACRO( SomeFunc(i)==true, {break;} ) }
会导致意外行为,因为中断会影响宏的 while 循环,而不是宏调用站点的 for 循环。void(0)
是一个错字,我的意思是(void)0
。而且我相信这确实解决了“悬空的 else”问题:请注意(void)0
之后没有分号。在这种情况下,悬空的 else(例如if (cond) if (1) foo() else (void)0 else { /* dangling else body */ }
)会触发编译错误。这是live example demonstrating it