在许多C / C宏中,我看到宏的代码包含在看似无意义的 do while 循环中 . 这是一些例子 .
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(false); 循环,但还有另一种解决方案不需要该构造 . 解决方案是使用逗号运算符:
do { ... } while(false);
#define FOO(X) (f(X),g(X))
甚至更加异乎寻常:
#define FOO(X) g((f(X),(X)))
虽然这适用于单独的指令,但它不适用于构造变量并将其用作 #define 的一部分的情况:
#define
#define FOO(X) (int s=5,f((X)+s),g((X)+s))
有了这个,就会被迫使用do / while结构 .
do ... while 和 if ... else 是为了使你的宏后面的分号总是意味着相同的东西 . 假设你有类似你的第二个宏 .
do ... while
if ... else
#define BAR(X) f(x); g(x)
现在,如果你在 if ... else 语句中使用 BAR(X); ,if语句的主体没有用大括号括起来,你会得到一个不好的惊喜 .
BAR(X);
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 版本将上面的代码扩展为以下代码,这在语法上是正确的 .
BAR
if (corge) f(corge), g(corge); else gralt();
如果代替 f(X) ,你有一个更复杂的代码体需要进入它自己的块,例如声明局部变量,这是行不通的 . 在最常见的情况下,解决方案是使用类似 do ... while 之类的东西来使宏成为一个单独的语句,它可以使用分号而不会产生混淆 .
f(X)
#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”,这可能会使现有悬挂的其他问题更难找到,就像在以下代码 .
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) ;
这意味着如果没有分号,用户希望编译器产生错误 .
但真正的正当理由是,在某些时候,宏的作者可能需要用真正的函数替换宏(也许是内联的) . 所以宏应该 really 表现得像一个 .
所以我们应该有一个需要分号的宏 .
如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 函数 .
bIsOk
g
这意味着我们必须向宏添加一个范围:
#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 / 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. }
@ jfm3 - 你对这个问题有一个很好的答案 . 您可能还想补充一点,宏语法也可以通过简单的“if”语句防止可能更危险(因为没有错误)的意外行为:
#define FOO(x) f(x); g(x) if (test) FOO( baz);
扩展为:
if (test) f(baz); g(baz);
这在语法上是正确的没有编译器错误,但可能会出现意外的后果,即g()将始终被调用 .
我发现这个技巧非常有用,在必须按顺序处理特定值的情况下 . 在每个处理级别,如果发生某些错误或无效条件,您可以避免进一步处理并提前中断 . 例如
#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);
do {} while (0) 用于 if (1) {} 的原因是,在将宏 do {} while (0) 调用为其他类型的块之前,没有人可以更改代码 . 例如,如果您调用 if (1) {} 包围的宏,如:
do {} while (0)
if (1) {}
else MACRO(x);
那实际上是 else if . 细微差别
else if
上述答案解释了这些结构的含义,但两者之间存在显着差异,未提及 . 事实上,有理由更喜欢 do ... while 到 if ... else 构造 .
if ... else 构造的问题是它不会强制你输出分号 . 喜欢这段代码:
FOO(1) printf("abc");
虽然我们遗漏了分号(错误地),但代码将扩展为
if (1) { f(X); g(X); } else printf("abc");
并将静默编译(尽管某些编译器可能会发出无法访问代码的警告) . 但 printf 语句永远不会被执行 .
printf
do ... while 构造没有这样的问题,因为 while(0) 之后唯一有效的标记是分号 .
while(0)
Jens Gustedt的P99 preprocessor library(是的,这样的事情存在的事实也引起了我的注意!)通过定义以下内容,以一种小但重要的方式改进了 if(1) { ... } else 构造:
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) 会产生语法错误,否则会跳过下一个块 . (这里实际上没有"dangling else"问题,因为 else 绑定到最近的 if ,这是宏中的那个 . )
do { ... } while(0)
break
continue
((void)0)
else
if
如果您对使用C预处理器可以或多或少安全地完成的各种事情感兴趣,请查看该库 .
出于某些原因,我无法对第一个答案发表评论......
你们中的一些人展示了带有局部变量的宏,但是没有人提到你们可以_59582_使用最常用的变量名 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循环中声明的局部变量 .
因此,永远不要在宏中使用常见的变量名!
我不认为它被提及所以考虑一下
while(i<100) FOO(i++);
将被翻译成
while(i<100) do { f(i++); g(i++); } while (0)
请注意宏如何评估 i++ 两次 . 这可能会导致一些有趣的错误 .
i++
do {} while (0) 和 if (1) {} else 是为了确保宏扩展到只有1条指令 . 除此以外:
if (1) {} else
if (something) FOO(X);
将扩大到:
if (something) f(X); g(X);
并且 g(X) 将在 if 控制语句之外执行 . 使用 do {} while (0) 和 if (1) {} else 时可以避免这种情况 .
g(X)
使用GNU statement expression(不是标准C的一部分),通过简单地使用 ({}) ,您有比 do {} while (0) 和 if (1) {} else 更好的方法来解决这个问题:
({})
#define FOO(X) ({f(X); g(X);})
此语法与返回值兼容(请注意 do {} while (0) 不是),如:
return FOO("X");
11 回答
虽然预计编译器会优化
do { ... } while(false);
循环,但还有另一种解决方案不需要该构造 . 解决方案是使用逗号运算符:甚至更加异乎寻常:
虽然这适用于单独的指令,但它不适用于构造变量并将其用作
#define
的一部分的情况:有了这个,就会被迫使用do / while结构 .
do ... while
和if ... else
是为了使你的宏后面的分号总是意味着相同的东西 . 假设你有类似你的第二个宏 .现在,如果你在
if ... else
语句中使用BAR(X);
,if语句的主体没有用大括号括起来,你会得到一个不好的惊喜 .上面的代码将扩展为
这在语法上是不正确的,因为else不再与if相关联 . 在宏中用大括号包装东西没有帮助,因为大括号后面的分号在语法上是不正确的 .
有两种方法可以解决问题 . 第一种是使用逗号来对宏中的语句进行排序,而不会使其具有像表达式一样的能力 .
上面的bar
BAR
版本将上面的代码扩展为以下代码,这在语法上是正确的 .如果代替
f(X)
,你有一个更复杂的代码体需要进入它自己的块,例如声明局部变量,这是行不通的 . 在最常见的情况下,解决方案是使用类似do ... while
之类的东西来使宏成为一个单独的语句,它可以使用分号而不会产生混淆 .你不必使用
do ... while
,你也可以用if ... else
做点什么,虽然当if ... else
在if ... else
内部扩展时它会导致“dangling else”,这可能会使现有悬挂的其他问题更难找到,就像在以下代码 .关键是在悬挂分号错误的情况下用掉分号 . 当然,在这一点上它可能(并且可能应该)被认为最好将
BAR
声明为实际函数,而不是宏 .总之,
do ... while
可以解决C预处理器的缺点 . 当那些C风格指南告诉你裁掉C预处理器时,这是他们担心的事情 .宏是复制/粘贴的文本,预处理器将放入正版代码中;宏的作者希望替换将产生有效的代码 .
有三个好的“提示”可以成功:
帮助宏的行为就像真正的代码一样
普通代码通常以分号结束 . 如果用户查看不需要的代码......
这意味着如果没有分号,用户希望编译器产生错误 .
但真正的正当理由是,在某些时候,宏的作者可能需要用真正的函数替换宏(也许是内联的) . 所以宏应该 really 表现得像一个 .
所以我们应该有一个需要分号的宏 .
生成有效代码
如jfm3的答案所示,有时宏包含多条指令 . 如果宏在if语句中使用,这将是有问题的:
此宏可以扩展为:
无论
bIsOk
的值如何,都将执行g
函数 .这意味着我们必须向宏添加一个范围:
生成有效代码2
如果宏是这样的:
我们可能在以下代码中遇到另一个问题:
因为它会扩展为:
当然,这段代码不会编译 . 所以,再一次,解决方案是使用范围:
代码再次正常运行 .
结合分支范围效应?
有一个C / C习语产生这种效果:do / while循环:
do / while可以创建一个范围,从而封装宏的代码,最后需要一个分号,从而扩展为需要一个代码的代码 .
奖金?
C编译器将优化do / while循环,因为其后置条件为false的事实在编译时是已知的 . 这意味着像一个宏:
将正确扩展为
然后编译和优化
@ jfm3 - 你对这个问题有一个很好的答案 . 您可能还想补充一点,宏语法也可以通过简单的“if”语句防止可能更危险(因为没有错误)的意外行为:
扩展为:
这在语法上是正确的没有编译器错误,但可能会出现意外的后果,即g()将始终被调用 .
我发现这个技巧非常有用,在必须按顺序处理特定值的情况下 . 在每个处理级别,如果发生某些错误或无效条件,您可以避免进一步处理并提前中断 . 例如
do {} while (0)
用于if (1) {}
的原因是,在将宏do {} while (0)
调用为其他类型的块之前,没有人可以更改代码 . 例如,如果您调用if (1) {}
包围的宏,如:那实际上是
else if
. 细微差别上述答案解释了这些结构的含义,但两者之间存在显着差异,未提及 . 事实上,有理由更喜欢
do ... while
到if ... else
构造 .if ... else
构造的问题是它不会强制你输出分号 . 喜欢这段代码:虽然我们遗漏了分号(错误地),但代码将扩展为
并将静默编译(尽管某些编译器可能会发出无法访问代码的警告) . 但
printf
语句永远不会被执行 .do ... while
构造没有这样的问题,因为while(0)
之后唯一有效的标记是分号 .Jens Gustedt的P99 preprocessor library(是的,这样的事情存在的事实也引起了我的注意!)通过定义以下内容,以一种小但重要的方式改进了
if(1) { ... } else
构造:这个的基本原理是,与
do { ... } while(0)
构造不同,break
和continue
仍在给定块内部工作,但如果在宏调用后省略分号,则((void)0)
会产生语法错误,否则会跳过下一个块 . (这里实际上没有"dangling else"问题,因为else
绑定到最近的if
,这是宏中的那个 . )如果您对使用C预处理器可以或多或少安全地完成的各种事情感兴趣,请查看该库 .
出于某些原因,我无法对第一个答案发表评论......
你们中的一些人展示了带有局部变量的宏,但是没有人提到你们可以_59582_使用最常用的变量名 i .
例如当下面的宏
用于以下功能
宏不会使用在some_func开头声明的预期变量i,而是使用在宏的do ... while循环中声明的局部变量 .
因此,永远不要在宏中使用常见的变量名!
我不认为它被提及所以考虑一下
将被翻译成
请注意宏如何评估
i++
两次 . 这可能会导致一些有趣的错误 .解释
do {} while (0)
和if (1) {} else
是为了确保宏扩展到只有1条指令 . 除此以外:将扩大到:
并且
g(X)
将在if
控制语句之外执行 . 使用do {} while (0)
和if (1) {} else
时可以避免这种情况 .更好的选择
使用GNU statement expression(不是标准C的一部分),通过简单地使用
({})
,您有比do {} while (0)
和if (1) {} else
更好的方法来解决这个问题:此语法与返回值兼容(请注意
do {} while (0)
不是),如: