为什么预处理器宏是邪恶的,有什么替代方案?

我一直在问这个问题,但是我从来没有得到一个真正好的答案; 我认为几乎所有的程序员在写第一个“ Hello World”之前都遇到过这样的短语,比如“宏不应该被使用”,“宏是邪恶的”等等,我的问题是: 为什么?有了新的 C + + 11,在这么多年之后还有真正的替代品吗?

The easy part is about macros like #pragma, that are platform specific and compiler specific, and most of the time they have serious flaws like #pragma once that is error prone in at least 2 important situation: same name in different paths and with some network setups and filesystems.

但是一般来说,宏和它们的替代用法怎么办呢?

60907 次浏览

一个常见的问题是:

#define DIV(a,b) a / b


printf("25 / (3+2) = %d", DIV(25,3+2));

它将打印10,而不是5,因为预处理器会这样展开它:

printf("25 / (3+2) = %d", 25 / 3 + 2);

这个版本更安全:

#define DIV(a,b) (a) / (b)

宏尤其对于创建泛型代码(宏的参数可以是任何东西)很有价值,有时候还带有参数。

此外,这段代码被放置(即插入)在使用宏的位置。

OTOH,类似的结果可以通过以下方法实现:

  • 重载函数(不同的参数类型)

  • 模板,在 C + + (通用参数类型和值)

  • 内联函数(将代码放在调用它们的位置,而不是跳转到单点定义——然而,这是对编译器的一个建议)。

至于为什么宏不好:

1)没有参数的类型检查(它们没有类型) ,所以很容易被误用 2)有时会扩展成非常复杂的代码,这些代码在预处理文件中很难识别和理解 3)在宏中编写容易出错的代码很容易,比如:

#define MULTIPLY(a,b) a*b

and then call

MULTIPLY(2+3,4+5)

膨胀

2 + 3 * 4 + 5(不是 into: (2 + 3) * (4 + 5))。

为了实现后者,你应该定义:

#define MULTIPLY(a,b) ((a)*(b))

根据我的经验,宏对于程序大小来说并不理想,而且很难调试。但如果小心使用,它们是好的。

通常,一个好的替代方案是泛型函数和/或内联函数。

我不认为使用预处理器定义或者你称之为宏的宏有什么错。

它们是 c/c + + 中的一种(元)语言概念,和其他任何工具一样,如果你知道自己在做什么,它们可以让你的生活变得更轻松。宏的问题在于,它们在你的 c/c + + 代码之前被处理,并且生成新的代码,这些代码可能是错误的,并且会导致编译器错误,而这些错误几乎是显而易见的。从好的方面来看,它们可以帮助您保持代码的整洁,如果使用得当,还可以节省大量的输入工作,所以这取决于个人偏好。

俗话说“宏是邪恶的”,通常指的是使用 # Definition,而不是 # 杂注。

具体而言,该表达式指的是这两种情况:

  • 将魔术数字定义为宏

  • 使用宏替换表达式

有了新的 C + + 11,在这么多年之后还有一个真正的替代品吗?

是的,对于上面列表中的项目(魔术数字应该用 const/Constexpr 定义,表达式应该用[ Normal/inline/template/inline template ]函数定义。

下面是将魔术数字定义为宏和用宏替换表达式(而不是定义计算这些表达式的函数)所引入的一些问题:

  • 当为魔术数字定义宏时,编译器不保留已定义值的类型信息。这可能导致编译警告(和错误) ,并使调试代码的人感到困惑。

  • 当定义宏而不是函数时,使用该代码的程序员希望它们像函数一样工作,但事实并非如此。

考虑下面的代码:

#define max(a, b) ( ((a) > (b)) ? (a) : (b) )


int a = 5;
int b = 4;


int c = max(++a, b);

在将 a 和 c 赋值给 c 之后,应该期望 a 和 c 为6(因为使用的是 std: : max 而不是宏)。相反,代码执行:

int c = ( ((++a) ? (b)) ? (++a) : (b) ); // after this, c = a = 7

除此之外,宏不支持命名空间,这意味着在代码中定义宏将限制客户机代码使用哪些名称。

This means that if you define the macro above (for max), you will no longer be able to #include <algorithm> in any of the code below, unless you explicitly write:

#ifdef max
#undef max
#endif
#include <algorithm>

使用宏代替变量/函数也意味着你不能获取它们的地址:

  • 如果宏作为常量的计算结果是一个神奇的数字,则不能通过地址传递它

  • 对于宏为函数的函数,不能将其用作谓词,不能获取函数的地址,也不能将其视为函数。

编辑: 作为一个例子,正确的替代 #define max以上:

template<typename T>
inline T max(const T& a, const T& b)
{
return a > b ? a : b;
}

This does everything the macro does, with one limitation: if the types of the arguments are different, the template version forces you to be explicit (which actually leads to safer, more explicit code):

int a = 0;
double b = 1.;
max(a, b);

如果将这个 max 定义为宏,则代码将进行编译(带有警告)。

如果这个 max 被定义为一个模板函数,编译器将指出其中的歧义,并且您必须说 max<int>(a, b)或者 max<double>(a, b)(从而显式地说明您的意图)。

宏就像任何其他工具一样——在谋杀中使用的锤子并不邪恶,因为它是锤子。它是邪恶的方式,这个人使用它的方式。如果你想钉钉子,锤子是一个完美的工具。

There are a few aspects to macros that make them "bad" (I'll expand on each later, and suggest alternatives):

  1. 不能调试宏。
  2. 宏观扩张会导致奇怪的副作用。
  3. Macros have no "namespace", so if you have a macro that clashes with a name used elsewhere, you get macro replacements where you didn't want it, and this usually leads to strange error messages.
  4. 宏可能会影响您没有意识到的事情。

So let's expand a little here:

1)宏无法调试。 当你有一个可以转换成数字或字符串的宏时,源代码会有宏的名字,而许多调试器不能“看到”这个宏转换成什么。所以你根本不知道发生了什么。

Replacement: Use enum or const T

For "function-like" macros, because the debugger works on a "per source line where you are" level, your macro will act like a single statement, no matter if it's one statement or a hundred. Makes it hard to figure out what is going on.

替换 : 如果需要“快速”,则使用函数-inline (但要注意,内联过多不是一件好事)

2)宏扩展会产生奇怪的副作用。

最著名的是 #define SQUARE(x) ((x) * (x))和使用 x2 = SQUARE(x++)。这导致了 x2 = (x++) * (x++);,即使它是有效的代码[1] ,也几乎肯定不是程序员想要的。如果它是一个函数,那么做 x + + 就可以了,而且 x 只增加一次。

另一个例子是宏中的“ if else”,比如说:

#define safe_divide(res, x, y)   if (y != 0) res = x/y;

然后

if (something) safe_divide(b, a, x);
else printf("Something is not set...");

事实上,它变成了完全错误的事情... 。

替换 : 实函数。

3) Macros have no namespace

如果我们有一个宏:

#define begin() x = 0

我们在 C + + 中有一些代码使用了 start:

std::vector<int> v;


... stuff is loaded into v ...


for (std::vector<int>::iterator it = myvector.begin() ; it != myvector.end(); ++it)
std::cout << ' ' << *it;

现在,您认为您会得到什么样的错误消息,以及在哪里查找错误(假设您已经完全忘记——或者甚至不知道——存在于其他人编写的某个头文件中的 start 宏) ?[更有趣的是,如果你在 include 之前包含了这个宏,那么当你查看代码本身的时候,你会发现一些奇怪的错误,这些错误完全没有意义。

替换 : 这里没有像“规则”那样多的替换——只对宏使用大写名称,对其他事情不要使用所有大写名称。

4)宏有你意识不到的效果

以下面这个函数为例:

#define begin() x = 0
#define end() x = 17
... a few thousand lines of stuff here ...
void dostuff()
{
int x = 7;


begin();


... more code using x ...


printf("x=%d\n", x);


end();


}

现在,不看宏,你会认为 start 是一个函数,它不会影响 x。

这类事情,我见过更复杂的例子,可以真正搞砸你的一天!

替换 : 不要使用宏来设置 x,或者将 x 作为参数传入。

有时候使用宏绝对是有益的。一个例子是用宏包装函数以传递文件/行信息:

#define malloc(x) my_debug_malloc(x, __FILE__, __LINE__)
#define free(x)  my_debug_free(x, __FILE__, __LINE__)

现在我们可以在代码中使用 my_debug_malloc作为常规 malloc,但是它有额外的参数,所以当它到达结尾时,我们扫描“哪些内存元素没有被释放”,我们可以打印分配的位置,这样程序员就可以追踪泄漏。

[1]“在序列点中”多次更新一个变量是未定义的行为。序列点并不完全等同于语句,但是对于大多数意图和目的,我们应该将其视为序列点。因此,这样做 x++ * x++将更新 x两次,这是未定义的,可能会导致不同的价值观在不同的系统,以及不同的结果值在 x

C/C + + 中的宏可以作为版本控制的重要工具。同样的代码可以交付给两个客户端,只需要进行一些宏配置。我用的是

#define IBM_AS_CLIENT
#ifdef IBM_AS_CLIENT
#define SOME_VALUE1 X
#define SOME_VALUE2 Y
#else
#define SOME_VALUE1 P
#define SOME_VALUE2 Q
#endif

如果没有宏,这种功能就不太可能实现。宏实际上是一个很好的软件配置管理工具,而不仅仅是一种 创建代码重用的快捷方式 宏中的可重用性肯定会产生问题。

当预处理器宏被用于以下目的时,它们并不邪恶:

  • 使用 # ifdef 类型的构造创建同一软件的不同版本,例如不同区域的窗口版本。
  • 用于定义代码测试相关值。

其他选择- 为了类似的目的,可以使用 ini、 xml、 json 格式的某种配置文件。但是使用它们会对代码产生运行时影响,而预处理器宏可以避免这种影响。