31

根据 C++ FAQ,宏是邪恶的

[9.5] 为什么我应该使用内联函数而不是普通的旧#define 宏?

因为#define宏在 4 种不同的方面是邪恶的:evil#1、evil#2、evil#3 和 evil#4。有时你无论如何都应该使用它们,但它们仍然是邪恶的。与#define宏不同,内联函数避免了臭名昭著的宏错误,因为内联函数总是对每个参数只计算一次。换句话说,调用内联函数在语义上就像调用常规函数一样,只是更快:

// A macro that returns the absolute value of i
#define unsafe(i)  \
        ( (i) >= 0 ? (i) : -(i) )

// An inline function that returns the absolute value of i
inline
int safe(int i)
{
  return i >= 0 ? i : -i;
}

int f();

void userCode(int x)
{
  int ans;

  ans = unsafe(x++);   // Error! x is incremented twice
  ans = unsafe(f());   // Danger! f() is called twice

  ans = safe(x++);     // Correct! x is incremented once
  ans = safe(f());     // Correct! f() is called once
}

与宏不同的是,它会检查参数类型,并正确执行必要的转换。

宏对您的健康有害;除非必须,否则不要使用它们。

有人可以解释为什么unsafe(x++)增量x两次吗?我无法弄清楚。

4

4 回答 4

69

通过预处理器运行它会显示问题。使用gcc -E(也可以使用cpp -P,该-P选项还抑制生成的#行),

inline
int safe(int i)
{
  return i >= 0 ? i : -i;
}

int f();

void userCode(int x)
{
  int ans;

  //    increment 1      increment 2 (one of these)
  //        |             |     |
  //        V             V     V
  ans = ( (x++) >= 0 ? (x++) : -(x++) );
  ans = ( (f()) >= 0 ? (f()) : -(f()) );

  ans = safe(x++);
  ans = safe(f());
}

正如艺术噪音所指出的,该函数f()也被unsafe宏调用了两次。也许它是纯粹的(没有副作用)所以它本身并没有错。但仍不理想。

因此,由于内联函数通常比类函数宏更安全,因为它们与其他基本元素在相同的语义级别上工作:变量和表达式;对于显式常量,enums 通常可以更整洁有什么好的用途?

设置仅在编译时已知的常量。您可以在编译时从命令行定义宏。代替

#define X 12

在源文件中,您可以添加

-DX=12

cc命令。您也可以#undef X从命令行使用-UX.

这允许条件编译之类的东西,例如。

#if X
   do this;
#else
   do that;
#endif
   while (loop);

由 makefile 控制,它本身可能由配置脚本生成。

X 宏。IMO X-Macros 最引人注目的用途是enum将标识符与可打印的字符串相关联。虽然一开始看起来很有趣,但它减少了这些并行定义的重复和同步问题。

#define NAMES(_) _(Alice) _(Bob) _(Caravaggio) _(DuncanIdaho)
#define BARE(_) _ ,
#define STRG(_) #_ ,
enum { NAMES(BARE) };
char *names[] = { NAMES(STRG) };

请注意,您可以将宏的名称作为参数传递给另一个宏,然后使用参数调用传递的宏,就好像它本身就是一个宏(因为它一个宏)。有关 X-Macros 的更多信息,请参阅此问题

于 2013-04-26T19:06:02.040 回答
17

宏在程序编译之前有效地进行复制/粘贴。

unsafe(x++)

会成为

( (x++) >= 0 ? (x++) : -(x++) )
于 2013-04-26T19:05:36.460 回答
10

预处理器在编译之前替换宏。

编译器看到这个:

  ( (x++) >= 0 ? (x++) : -(x++) )
于 2013-04-26T19:05:29.920 回答
4

unsafe(x)计算表达式x两次。一次确定其真值,然后在三元运算符的两个分支之一中进行第二次。内联函数safe接收一个评估参数:表达式在函数调用之前被评估一次,并且函数调用与局部变量一起工作。

unsafe实际上并不像它可能的那样不安全。三元运算符在评估测试和评估结果表达式或替代表达式之间引入了一个序列点。unsafe(x++)可靠地增加x两次,当然,问题是这种行为是出乎意料的。通常,多次扩展表达式的宏没有这种保证。通常,它们会产生完全未定义的行为!

大约在 1999 年,我制作了一个库模块模块,用于捕捉具有副作用的宏的使用。

因此,您可以编写“邪恶”宏并使用它们,机器会捕捉到它们被意外与具有副作用的参数一起使用的情况(前提是您有足够的代码覆盖率来在运行时达到这些用途)。

这是测试程序,unsafe.c. 请注意,它包含一个头文件sfx.h并在扩展标记序列中使用一个SFX_CHECKunsafe

#include "sfx.h"

#define unsafe(i)  \
          ( (SFX_CHECK(i)) >= 0 ? (i) : -(i) )

inline
int safe(int i)
{
  return i >= 0 ? i : -i;
}

int f(void)
{
  return 0;
}

int main(void)
{
  int ans;
  int x = 0;

  ans = unsafe(x++);   // Error! x is incremented twice
  ans = unsafe(f());   // Danger! f() is called twice

  ans = safe(x++);     // Correct! x is incremented once
  ans = safe(f());     // Correct! f() is called once
}

我们编译所有内容并从 Linux shell 提示符运行:

$ gcc unsafe.c hash.c except.c sfx.c -o unsafe
$ ./unsafe
unsafe.c:22: expression "x++" has side effects
unsafe.c:23: expression "f()" may have side effects

请注意,这x++肯定有副作用,而f可能有也可能没有。所以消息的措辞不同。调用两次的函数不一定是问题,因为函数可能是函数(没有副作用)。

如果你对它的工作原理感到好奇,你可以在这里得到它。如果启用调试,则会有运行时损失;当然SFX_CHECK可以禁用,所以它什么都不做(类似于assert)。

第一次SFX_CHECK评估受保护的表达式时,会对其进行解析以确定它是否可能具有副作用。因为这种解析是在没有任何访问符号表信息(如何声明标识符)的情况下完成的,所以它是模棱两可的。解析器追求多种解析策略。回溯是使用基于setjmp/longjmp. 解析结果存储在以文本表达式为键的哈希表中,以便在将来的评估中更快地检索。

于 2013-04-26T22:03:46.427 回答