ChatGPT解决这个技术问题 Extra ChatGPT

为什么在宏中使用看似毫无意义的 do-while 和 if-else 语句?

在许多 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)
对于带有 else 的示例,我将在末尾添加一个 void 类型的表达式...例如 ((void)0)
请注意,do while 构造与 return 语句不兼容,因此 if (1) { ... } else ((void)0) 构造在标准 C 中具有更兼容的用法。在 GNU C 中,您会更喜欢我的回答中描述的构造。

P
Pavel

do ... whileif ... 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 ... elseif ... else 内展开时,它会导致一个“dangling else”,这可能会使现有的悬空 else 问题更难找到,如下面的代码所示。

if (corge)
  if (1) { f(corge); g(corge); } else;
else
  gralt();

关键是在悬挂分号错误的情况下用完分号。当然,在这一点上可以(并且可能应该)争辩说,将 BAR 声明为实际函数而不是宏会更好。

总之,do ... while 可以解决 C 预处理器的缺点。当那些 C 风格指南告诉你放弃 C 预处理器时,这就是他们担心的事情。


这不是在 if、while 和 for 语句中始终使用大括号的有力论据吗?如果您强调始终这样做(例如 MISRA-C 所要求的),上述问题就会消失。
逗号示例应该是 #define BAR(X) (f(X), g(X)) 否则运算符优先级可能会混淆语义。
@DawidFerenczy:虽然四年半前的你和我都提出了一个很好的观点,但我们必须生活在现实世界中。除非我们可以保证代码中的所有 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 循环。
@ace void(0) 是一个错字,我的意思是 (void)0。而且我相信这确实解决了“悬空的 else”问题:请注意 (void)0 之后没有分号。在这种情况下,悬空的 else(例如 if (cond) if (1) foo() else (void)0 else { /* dangling else body */ })会触发编译错误。这是live example demonstrating it
V
VLL

宏是预处理器将放入正版代码中的文本的复制/粘贴片段;宏的作者希望替换将产生有效的代码。

成功的三个好“技巧”:

帮助宏表现得像真正的代码

普通代码通常以分号结尾。如果用户查看代码不需要...

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.
}

请注意,将宏更改为内联函数会更改一些标准的预定义宏,例如,以下代码显示了 FUNCTION 和 LINE 的更改: #include #define Fmacro() printf("%s %d\n", FUNCTION, LINE) inline void Finline() { printf("%s %d\n", FUNCTION, LINE); } int main() { Fmacro();芬线();返回0; (粗体字应该用双下划线括起来——格式错误!)
这个答案有许多次要但并非完全无关紧要的问题。例如: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。
@Gnubie:我想你还在这里,但你现在还没有弄清楚:你可以在评论中用反斜杠转义星号和下划线,所以如果你输入 \_\_LINE\_\_ 它会呈现为 __LINE__。恕我直言,最好只对代码使用代码格式;例如,__LINE__(不需要任何特殊处理)。 PS我不知道这在2012年是否属实;从那时起,他们对引擎进行了相当多的改进。
感谢我的评论晚了六年,但大多数 C 编译器实际上并没有内联 inline 函数(标准允许)
M
Michael Burr

@jfm3 - 你对这个问题有一个很好的答案。您可能还想补充一点,宏成语还可以通过简单的“if”语句防止可能更危险(因为没有错误)的意外行为:

#define FOO(x)  f(x); g(x)

if (test) FOO( baz);

扩展为:

if (test) f(baz); g(baz);

这在语法上是正确的,因此没有编译器错误,但可能会导致始终调用 g() 的意外后果。


Y
Yakov Galka

上面的答案解释了这些构造的含义,但两者之间存在显着差异,没有提到。事实上,有理由更喜欢 do ... while 而不是 if ... else 构造。

if ... else 构造的问题在于它不会强制您输入分号。就像在这段代码中:

FOO(1)
printf("abc");

虽然我们省略了分号(错误地),但代码将扩展为

if (1) { f(X); g(X); } else
printf("abc");

并将静默编译(尽管某些编译器可能会针对无法访问的代码发出警告)。但是 printf 语句永远不会被执行。

do ... while 构造没有这样的问题,因为在 while(0) 之后的唯一有效标记是分号。


@RichardHansen:仍然没有那么好,因为从宏调用来看,您不知道它是扩展为语句还是表达式。如果有人假设后者,她可能会写 FOO(1),x++; 这将再次给我们一个误报。只需使用 do ... while 即可。
记录宏以避免误解就足够了。我同意 do ... while (0) 更可取,但它有一个缺点:breakcontinue 将控制 do ... while (0) 循环,而不是包含宏调用的循环。所以 if 技巧仍然有价值。
我看不出您可以将被视为宏 do {...} while(0) 伪循环内的 breakcontinue 放在哪里。即使在宏参数中也会产生语法错误。
使用 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() 调用的循环。
C
Community

解释

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");

在宏中使用块钳制 {} 就足以捆绑宏代码,以便针对相同的 if 条件路径执行所有代码。 do-while around 用于在宏被使用的地方强制使用分号。因此,宏被强制执行更多类似的功能。这包括使用时对尾随分号的要求。
M
Marius

虽然预计编译器会优化掉 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 构造。


谢谢,由于逗号运算符不保证执行顺序,这种嵌套是一种强制执行顺序的方法。
@马吕斯:错;逗号运算符是一个序列点,因此确实保证了执行顺序。我怀疑您将它与函数参数列表中的逗号混淆了。
只是想补充一点,编译器被迫保留程序可观察的行为,因此优化 do/while away 并不是什么大问题(假设编译器优化是正确的)。
@MarcoA。虽然你是对的,但我过去发现编译器优化虽然完全保留了代码的功能,但通过改变在单一上下文中似乎无济于事的行,会破坏多线程算法。第 Peterson's Algorithm 点的例子。
这也不适用于所有类型的构造,尽管带有三元运算符和 this 的 C 语言相当有表现力。
I
Isaac Schwabacher

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) 构造不同,breakcontinue 仍然在给定块内工作,但是如果在宏调用之后省略分号,((void)0) 会产生语法错误,否则会跳过下一个块。 (这里实际上没有“悬空的 else”问题,因为 else 绑定到最近的 if,它是宏中的那个。)

如果您对可以使用 C 预处理器或多或少安全地完成的各种事情感兴趣,请查看该库。


虽然非常聪明,但这会导致编译器警告轰炸其他潜在的悬空。
您通常使用宏来创建一个包含的环境,也就是说,您永远不会在宏内部使用 break(或 continue)来控制在外部开始/结束的循环,这只是一种糟糕的风格并隐藏了潜在的退出点。
Boost 中还有一个预处理器库。它有什么令人兴奋的地方?
else ((void)0) 的风险在于有人可能正在编写 YOUR_MACRO(), f();,它在语法上是有效的,但永远不要调用 f()do while 是语法错误。
@melpomene 那么else do; while (0)呢?
M
Mike Meyer

由于某些原因,我无法对第一个答案发表评论......

你们中的一些人展示了带有局部变量的宏,但没有人提到你不能只在宏中使用任何名称!总有一天它会咬用户!为什么?因为输入参数被替换到您的宏模板中。在您的宏示例中,您使用了可能最常用的变量名称 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;
@Blaisorblade:实际上这是不正确且非法的 C;前导下划线保留供实现使用。您看到这种“通常模式”的原因是读取系统标头(“实现”),它必须将自己限制在这个保留的命名空间中。对于应用程序/库,您应该选择自己的晦涩、不易冲突且不带下划线的名称,例如 mylib_internal___i 或类似名称。
@R ..你是对的 - 我实际上已经在 Linux 内核的“应用程序”中读到了这个,但无论如何它都是一个例外,因为它没有使用标准库(从技术上讲,是一个“独立的”C 实现一个“托管”的)。
@R .. 这不太正确:前导下划线后跟大写或第二个下划线保留用于所有上下文中的实现。前导下划线后跟其他内容不保留在本地范围内。
@Leushenko:是的,但是区别非常微妙,我发现最好告诉人们根本不要使用这样的名字。了解其中微妙之处的人大概已经知道我在掩饰细节。 :-)
J
John Nilsson

我不认为它被提及所以考虑这个

while(i<100)
  FOO(i++);

会被翻译成

while(i<100)
  do { f(i++); g(i++); } while (0)

请注意宏如何对 i++ 进行两次评估。这可能会导致一些有趣的错误。


这与 do ... while(0) 构造无关。
真的。但是与宏与函数的主题以及如何编写充当函数的宏有关...
与上述类似,这不是答案,而是评论。主题:这就是为什么你只使用一次东西:do { int macroname_i = (i); f(macroname_i); g(macroname_i); } while (/* CONSTCOND */ 0)