在许多C/C++宏中,我看到宏的代码被封装在一个似乎没有意义的EDCOX1×0循环中。下面是一些例子。
1 2
| #define FOO(X) do { f(X); g(X); } while (0)
#define FOO(X) if (1) { f(X); g(X); } else |
我看不出do while在做什么。为什么不直接写这个呢?
1
| #define FOO(X) f(X); g(X) |
- 对于使用else的示例,我将在末尾添加一个void类型的表达式…喜欢((空)0)。
- 提醒您,do while构造与RETURN语句不兼容,因此if (1) { ... } else ((void)0)构造在标准C中有更兼容的用法。在GNU C中,您更喜欢我的答案中描述的构造。
do ... while和if ... else在那里使它成为宏后的分号始终表示相同的内容。让我们说你有点像你的第二个宏。
1
| #define BAR(X) f(x); g(x) |
现在,如果你在一个if ... else语句中使用BAR(X);,其中if语句的主体没有用花括号括起来,你会得到一个坏的惊喜。
1 2 3 4
| if (corge)
BAR(corge);
else
gralt(); |
上述代码将扩展为
1 2 3 4
| if (corge)
f(corge); g(corge);
else
gralt(); |
这在语法上是不正确的,因为其他的不再与if关联。在宏中用大括号括起来没有帮助,因为大括号后面的分号在语法上不正确。
1 2 3 4
| if (corge)
{f(corge); g(corge);};
else
gralt(); |
有两种方法可以解决问题。第一种方法是使用逗号对宏中的语句进行排序,而不破坏它像表达式一样执行操作的能力。
1
| #define BAR(X) f(X), g(X) |
以上版本的bar BAR将上述代码扩展为以下代码,这在语法上是正确的。
1 2 3 4
| if (corge)
f(corge), g(corge);
else
gralt(); |
如果您有一个更复杂的代码体需要放入自己的块(例如声明局部变量),而不是f(X),那么这不起作用。在最一般的情况下,解决方案是使用像do ... while这样的东西,使宏成为一个单独的语句,它使用分号而不会混淆。
1 2 3 4
| #define BAR(X) do { \
int i = f(X); \
if (i > 4) g(i); \
} while (0) |
你不必使用do ... while,你也可以用if ... else做一些东西,尽管当if ... else在if ... else的内部扩展时,它会导致一个"悬而未决的其他"问题,这可能使现有的悬而未决的其他问题更难找到,如下面的代码所示。
1 2 3 4
| if (corge)
if (1) { f(corge); g(corge); } else;
else
gralt(); |
重点是在悬空分号错误的情况下使用分号。当然,在这一点上,我们可以(也可能应该)提出这样的观点:最好将BAR声明为实际函数,而不是宏。
总之,do ... while是用来解决C预处理器的缺点的。当那些C样式的指南告诉您停止使用C预处理器时,这是他们担心的事情。
- 我自己的答案错过了"else problem if braces and semi colon",但是您忘记了范围问题,以及编译程序对do/while的优化(false)。-p…
- 也许是你不应该这样做的最好例子…
- 但是为什么不简单地定义foo(x)f(x);g(x);?
- @科罗纳:如果(表达式)foo(x);否则…此类代码将无效,因为if子句中有2条语句。
- 在if、while和for语句中总是使用大括号,这不是一个有力的论点吗?如果你总是强调这样做(例如,正如misra-c所要求的那样),上面描述的问题就会消失。
- 为什么作为一个程序员,我在学习了这些之后感觉更糟…
- 逗号示例应该是#define BAR(X) (f(X), g(X)),否则运算符优先级可能会破坏语义。
- 你不能通过做#define FOO(x) if (1) { ... } else (void)0来避免悬而未决的其他问题吗?
- 正如@stevenmelnikoff所说,每个提到的问题都应该通过非常简单的修复来解决——在if/while/for语句的每个分支中添加括号,即使它们只包含一个语句。我主要是PHP程序员,但我总是到处使用大括号,即使我没有这样的问题):到处使用括号应该比使用这样的do/while(0)构造更容易,还是我错了?
- @达维德费伦奇:尽管四年半前你和我都说得对,但我们必须生活在现实世界中。除非我们能够保证代码中的所有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循环。
- @chriskline void(0)不是有效的C代码。而且,它仍然不能解决悬空的其他问题。
- @ace void(0)是一个打字错误,我的意思是(void)0。我相信这确实解决了"摇摆不定"的问题:注意在(void)0后面没有分号。在这种情况下,挂起的其他代码(例如if (cond) if (1) foo() else (void)0 else { /* dangling else body */ })会触发编译错误。下面是一个实况例子
- 问题不在于块后不能有分号,而是分号是它自己的语句,所以else在语法上仍然是错误的。
宏是复制/粘贴的文本,预处理程序会将其放入真正的代码中;宏的作者希望替换后的代码能产生有效的代码。
成功的秘诀有三个:
帮助宏像真正的代码一样工作
正常代码通常以分号结尾。如果用户查看不需要的代码…
1 2 3
| doSomething(1) ;
DO_SOMETHING_ELSE(2) // <== Hey? What's this?
doSomethingElseAgain(3) ; |
这意味着如果不存在分号,用户希望编译器产生错误。
但真正的好理由是,在某个时候,宏的作者可能需要用真正的函数(可能是内联的)替换宏。所以宏的行为应该像宏一样。
所以我们应该有一个需要分号的宏。
生成有效代码
如JFM3的答案所示,有时宏包含多条指令。如果在if语句中使用宏,这将是有问题的:
1 2
| if(bIsOk)
MY_MACRO(42) ; |
此宏可以扩展为:
1 2 3 4
| #define MY_MACRO(x) f(x) ; g(x)
if(bIsOk)
f(42) ; g(42) ; // was MY_MACRO(42) ; |
无论bIsOk的值如何,都将执行g功能。
这意味着我们必须向宏添加一个作用域:
1 2 3 4
| #define MY_MACRO(x) { f(x) ; g(x) ; }
if(bIsOk)
{ f(42) ; g(42) ; } ; // was MY_MACRO(42) ; |
生成有效代码2
如果宏类似于:
1
| #define MY_MACRO(x) int i = x + 1 ; f(i) ; |
我们可能在以下代码中有另一个问题:
1 2 3 4 5
| void doSomething()
{
int i = 25 ;
MY_MACRO(32) ;
} |
因为它将扩展为:
1 2 3 4 5
| void doSomething()
{
int i = 25 ;
int i = 32 + 1 ; f(i) ; ; // was MY_MACRO(32) ;
} |
当然,这段代码不会编译。因此,解决方案再次使用了一个范围:
1 2 3 4 5 6 7
| #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循环:
1 2 3 4 5
| do
{
// code
}
while(false) ; |
do/while可以创建一个作用域,从而封装宏的代码,最后需要一个分号,从而扩展为需要一个分号的代码。
奖金?
C++编译器将优化Do/while循环,因为它的后置条件为false的事实在编译时是已知的。这意味着宏类似于:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #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.
} |
将正确展开为
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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.
} |
然后编译并优化为
1 2 3 4 5 6 7 8 9 10 11
| void doSomething(bool bIsOk)
{
int i = 25 ;
if(bIsOk)
{
f(43) ; g(43) ;
}
// Etc.
} |
- 请注意,将宏更改为内联函数会更改一些标准的预定义宏,例如,以下代码显示函数和行的更改:includedefine fmacro()printf("%s%d",function,line)inline void finline()printf("%s%d",function,line);int main()fmacro();finline();return 0;(粗体terMS应该用双下划线括起来-格式设置错误!)
- 这个答案有许多小问题,但并非完全无关紧要。例如: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's和未经交叉的T's。
- @Gnubie:我想你还在这里,到目前为止你还没有弄清楚:你可以用反斜杠在评论中转义星号和下划线,所以如果你输入\_\_LINE\_\_,它会显示为uuu line_uuuuuu.?imho,最好只对代码使用代码格式;例如,__LINE__(不需要任何特殊处理)。????????????????????????附笔。?我?不要?知道这在2012年是否是真的;从那时起他们对发动机做了很多改进。
- 感谢我的评论迟了六年,但大多数C编译器实际上并没有内联inline函数(如标准允许的那样)。
@你对这个问题有一个很好的答案。您可能还想补充一点,宏习惯用法还可以通过简单的"if"语句防止可能更危险的(因为没有错误)意外行为:
1 2 3
| #define FOO(x) f(x); g(x)
if (test) FOO( baz); |
扩展到:
1
| if (test) f(baz); g(baz); |
它在语法上是正确的,因此不存在编译器错误,但可能会导致意外的结果,即始终调用g()。
- "可能是无意的"?我会说"当然是无意的",否则程序员就需要被带出去枪毙(而不是被鞭打)。
- 或者他们可能会得到加薪,如果他们为一个三字母的机构工作并且秘密地将代码插入一个广泛使用的开源程序…-)
- 这个评论让我想起了最近在苹果操作系统中发现的sslcertverificationbug中的goto fail一行。
上述答案解释了这些结构的含义,但未提及的两个结构之间存在显著差异。事实上,有理由选择do ... while而不是if ... else结构。
if ... else构造的问题是它不会强制您放置分号。就像这段代码:
尽管我们遗漏了分号(错误地),但代码将扩展到
1 2
| if (1) { f(X); g(X); } else
printf("abc"); |
并且将静默编译(尽管有些编译器可能对无法访问的代码发出警告)。但printf声明永远不会执行。
do ... while构造没有这样的问题,因为while(0)后面唯一有效的标记是分号。
- 这就是为什么你应该这样做。
- @理查德汉森:仍然不是很好,因为从宏调用的角度来看,你不知道它是扩展到语句还是表达式。如果有人认为是后一种情况,她可能会写信给FOO(1),x++;,这将再次给我们一个假阳性。只需使用EDOCX1[1]就可以了。
- 记录宏观数据以避免误解就足够了。我同意do ... while (0)是可取的,但它有一个缺点:break或continue将控制do ... while (0)循环,而不是包含宏调用的循环。因此,江户十一〔六〕把戏仍然有价值。
- 我看不出在哪里可以放置一个break或continue,这将被视为您的宏do {...} while(0)伪循环的内部。即使在宏参数中,它也会产生语法错误。
- 使用do { ... } while(0)而不是if whatever构造的另一个原因是它的惯用性质。do {...} while(0)结构广泛、广为人知,并被许多程序员广泛使用。它的基本原理和文档是众所周知的。对于if构造则不是这样。因此,在进行代码审查时,不必费力地摸索。
- @Tristopia:我见过有人编写宏,将代码块作为参数(我不一定推荐)。例如:#define CHECK(call, onerr) if (0 != (call)) { onerr } else (void)0。它可以像CHECK(system("foo"), break;);那样使用,其中break;用于引用包围CHECK()调用的循环。
虽然预计编译器会优化掉do { ... } while(false);循环,但还有另一种解决方案不需要这种构造。解决方案是使用逗号运算符:
1
| #define FOO(X) (f(X),g(X)) |
或者更离奇地说:
1
| #define FOO(X) g((f(X),(X))) |
虽然这在单独的指令中可以很好地工作,但在构造变量并将其用作#define的一部分的情况下,它将无法工作:
1
| #define FOO(X) (int s=5,f((X)+s),g((X)+s)) |
使用此方法,将强制使用do/while构造。
- (f(x),(x))是一个非常可爱的技巧!
- 谢谢,因为逗号运算符不能保证执行顺序,所以这种嵌套是强制执行顺序的一种方法。
- @马吕斯:错;逗号运算符是一个序列点,因此可以保证执行顺序。我怀疑你把它和函数参数列表中的逗号混淆了。
- 第二个异国情调的建议成就了我的一天。
- 只是想补充一点,编译器必须保留程序可观察的行为,所以优化do/while away并不重要(假设编译器优化是正确的)。
- @ MarcoA。虽然您是正确的,但我在过去发现编译器优化,虽然精确地保留了代码的功能,但是通过改变在单一上下文中似乎什么都不做的行,将打破多线程算法。以Peterson's Algorithm点为例。
- 这也不适用于所有类型的构造,尽管C与三元运算符和this具有相当的表达性。
Jens Gustedt的P99预处理器库(是的,这种东西的存在也让我大吃一惊!)通过定义以下内容,以小而重要的方式改进了if(1) { ... } else结构:
1 2 3
| #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绑定,后者是宏中的if。)
如果您对使用C预处理器可以或多或少地安全地完成的事情感兴趣,请查看该库。
- 虽然非常聪明,但这会导致一个被编译器警告的潜在悬空的其他人轰炸。
- 通常使用宏来创建一个包含的环境,也就是说,在宏内部决不使用break或continue来控制在外部开始/结束的循环,这只是一种不好的样式,并且隐藏了潜在的出口点。
- Boost中还有一个预处理器库。什么让人心烦意乱?
出于某些原因,我无法对第一个答案发表评论…
有些人展示了带有局部变量的宏,但没有人提到不能在宏中使用任何名称!总有一天它会咬到用户的!为什么?因为输入参数被替换到宏模板中。在您的宏示例中,您使用了可能最常用的变量名i。
例如,当以下宏
1
| #define FOO(X) do { int i; for (i = 0; i < (X); ++i) do_something(i); } while (0) |
在以下函数中使用
1 2 3 4 5
| void some_func(void) {
int i;
for (i = 0; i < 10; ++i)
FOO(i);
} |
宏不会使用在某些函数开头声明的预期变量i,而是使用在do中声明的局部变量。宏的while循环。
因此,不要在宏中使用通用变量名!
- 通常的模式是在宏的变量名中添加下划线,例如int __i;。
- @blaisorblade:实际上这是不正确的和非法的c;前导下划线保留给实现使用。您看到这个"常规模式"的原因是读取系统头("实现"),它必须限制自己使用这个保留的名称空间。对于应用程序/库,您应该选择自己的模糊的、不太可能与没有下划线的名称发生冲突的名称,例如mylib_internal___i或类似名称。
- @ R.。你说得对-我在Linux内核的"应用程序"中读到过这个,但是无论如何它都是一个例外,因为它不使用标准库(从技术上讲,是一个"独立的"C实现,而不是一个"托管的"实现)。
- @ R.。这并不完全正确:在所有上下文中,都为实现保留了前导下划线和一个大写或第二个下划线。在本地作用域中不保留前导下划线和其他内容。
- @列申科:是的,但是这种区别非常微妙,我觉得最好的办法就是告诉人们不要使用这种名字。那些了解细节的人大概已经知道我在掩盖细节。-)
- 虽然正确,但这不是问题的答案。根据站点规则,请将其写进评论;如果你不能,请首先从好的答案(实际上是答案)中收集业力;是的,第一步可能很难,但是在这一步上,有很多可能。
解释
do {} while (0)和if (1) {} else是为了确保宏只扩展到1条指令。否则:
将扩展到:
1 2
| if (something)
f(X); g(X); |
而g(X)将在if控制声明之外执行。在使用do {} while (0)和if (1) {} else时,可以避免这种情况。
更好的选择
对于GNU语句表达式(不是标准C的一部分),您有一种比do {} while (0)和if (1) {} else更好的方法来解决这个问题,只需使用({})即可:
1
| #define FOO(X) ({f(X); g(X);}) |
该语法与返回值(注意,do {} while (0)不是)兼容,如:
- 在宏中使用块夹持将足以绑定宏代码,以便对相同的if条件路径执行所有操作。do while-around用于在宏被使用的地方强制使用分号。因此,宏的行为更为相似。这包括使用时对尾随分号的要求。
我不认为有人提到过,所以考虑一下
会被翻译成
1 2
| while(i<100)
do { f(i++); g(i++); } while (0) |
注意如何用宏对i++进行两次计算。这会导致一些有趣的错误。
- 这与做什么无关…while(0)构造。
- 真的。但与宏与函数的关系以及如何编写一个表现为函数的宏…
- 与上述情况类似,这不是一个答案,而是一个评论。主题:这就是为什么你只使用一次东西:do { int macroname_i = (i); f(macroname_i); g(macroname_i); } while (/* CONSTCOND */ 0)
我发现这个技巧非常有用,在您必须按顺序处理特定值的情况下。在每一个处理级别,如果发生了一些错误或无效的情况,您可以避免进一步的处理并提前中断。例如
1 2 3 4 5 6 7
| #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); |
- 我也看到它是这样使用的。
- 我宁可使用if (process_first() && process_second() && process_third()) { // do whatever, all success },也可以自由使用函数参数?这也为其他成功回报值(如>= 0)提供了灵活性,而无需创建额外的宏。
- 这也不是问题的真正答案…
- 如果有if-else序列并将这样一个宏放入if块中,那么这就不能很好地工作。
在if (1) {}上使用do {} while (0)的原因是,在将宏do {} while (0)调用为其他类型的块之前,没有任何方法可以更改代码。例如,如果调用一个由if (1) {}包围的宏,例如:
这实际上是一个else if。细微差别