在许多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)
在许多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 ... 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
版本将上述代码扩展为以下语法正确的代码。
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
中扩展时,它会导致 "悬挂 else" 的问题,这可能会使现有的悬挂 else 问题更难以找到,就像以下代码中所示。
if (corge)
if (1) { f(corge); g(corge); } else;
else
gralt();
关键在于在禁止出现悬挂分号的情况下使用分号。 当然,此时可能会(也应该)争辩说最好将BAR
声明为实际函数而不是宏。
总之,do ... while
的存在是为了解决C预处理器的缺陷。当那些C风格指南告诉你要避免使用C预处理器时,他们担心的正是这种情况。
#define BAR(X) (f(X), g(X))
否则,运算符优先级可能会混淆语义。 - Stewartdo {...} while(0)
形式的宏来说,使用if(1) {...} else void(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;} ) }
会导致意外的行为,因为break语句影响的是宏的while循环而不是宏调用点处的for循环。 - Chris Klinevoid(0)
是一个笔误,我想表达的是 (void)0
。我认为这确实解决了“悬挂else”问题:请注意,在 (void)0
后面没有分号。在那种情况下出现的悬挂 else(例如 if (cond) if (1) foo() else (void)0 else { /* dangling else body */ }
)会触发编译错误。这里有一个演示例子。 - Chris Kline宏是文本复制/粘贴的代码片段,预处理器会将其放入真实代码中;宏的作者希望替换后能产生有效的代码。
有三个好的“技巧”可以成功实现这一点:
通常,正常的代码以分号结尾。如果用户查看的是不需要分号的代码......
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 {
//macro code
} while(0);
do/while循环可以创建作用域,从而封装宏的代码,并且需要一个分号结束,因此会扩展成需要一个分号的代码。
额外的好处是?
C++编译器将优化掉do/while循环,因为其后置条件在编译时已知为假。这意味着像下面这样的宏:
do {
//宏定义的代码
} while(0);
#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)
的展开结果是什么。虽然大部分是正确的,但还有些需要修改。 - Jonathan Leffler\_\_LINE\_\_
,它会被渲染为__LINE__。我的看法是,最好对代码使用代码格式;例如,__LINE__
(不需要任何特殊处理)。P.S. 我不知道这个在2012年是否正确;他们从那时起对引擎进行了很多改进。 - Scott - Слава Україніif
语句中,你需要一个while(0)
才能正确运行。但是他们没有涵盖(而你做到了)为什么将宏体放在作用域中,例如#define FOO(x) { bar(x); }
并不足够简单。看起来你需要while(0)
的真正原因是要求在语句之后加上分号。 - Snackoverflow@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)
之后唯一有效的标记是分号。
#define FOO(X) if (1) { f(X); g(X); } else (void)0
- Richard HansenFOO(1),x++;
,这将再次给我们带来误报。只需使用do ... while
就可以了。 - Yakov Galkado ... while (0)
更可取,但它有一个缺点:break
或continue
将控制do ... while (0)
循环,而不是包含宏调用的循环。因此,if
技巧仍然有价值。 - Richard Hansendo {...} while(0)
中放置break
或continue
语句,即使在宏参数中这样做也会导致语法错误。 - Patrick Schlüterdo {...} while(0)
而不是 if
结构的另一个原因是它的惯用性。许多程序员广泛地使用 do {...} while(0)
结构,这个结构很有名并被广泛使用。它的原理和文档也很容易获得。相比之下,if
结构就不是那么容易理解了。因此,在进行代码审查时,使用 do {...} while(0)
会更加简单。 - Patrick Schlüter#define CHECK(call, onerr) if (0 != (call)) { onerr } else (void)0
。可以像这样使用:CHECK(system("foo"), break;);
,其中 break;
旨在引用包含 CHECK()
调用的循环。 - Richard Hansendo {} while (0)
和if (1) {} else
的作用是确保宏展开为仅有一条指令。否则:
if (something)
FOO(X);
将会扩展为:
if (something)
f(X); g(X);
使用 ({})
可以比 do {} while (0)
和 if (1) {} else
更好地解决此问题。 这是通过使用GNU的 语句表达式(不是标准C的一部分)来实现的:
#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结构。
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”问题,因为else
绑定到最近的if
,即宏中的那个。)
如果您对可以在C预处理器中相对安全地完成的事情感兴趣,请查看该库。
break
(或 continue
)来控制在外部开始/结束的循环,这只是一种糟糕的风格,并且隐藏了潜在的退出点。 - mirabiloselse ((void)0)
的风险在于,有人可能会写 YOUR_MACRO(), f();
,这是语法上有效的,但永远不会调用 f()
。而使用 do while
则会导致语法错误。 - melpomeneelse do; while (0)
怎么样? - Carl Lei由于某些原因,我无法在第一个答案上进行评论...
有些人展示了带有局部变量的宏,但没有人提到您不能在宏中使用任何名称!这会在某一天咬住用户!为什么?因为输入参数将替换为您的宏模板。而在您的宏示例中,您使用了可能是最常用的变量名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;
。 - Blaisorblademylib_internal___i
或类似名称。 - R.. GitHub STOP HELPING ICE我认为这个问题没有被提及,所以请考虑一下:
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)
- mirabilos
void
类型的表达式,例如 **((void)0)**。 - Phil1970do while
结构与返回语句不兼容,因此在标准 C 中,if (1) { ... } else ((void)0)
结构具有更兼容的用法。而在 GNU C 中,您会更喜欢我回答中描述的结构。 - Cœur