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

新手上路,请多包涵

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

简单的部分是关于 #pragma 类的宏,它们是特定于平台和编译器的,并且大多数时候它们有严重的缺陷,例如 #pragma once 这在至少两种重要情况下容易出错: 在不同的路径和一些网络设置和文件系统中具有相同的名称。

但总的来说,宏及其用法的替代品呢?

原文由 user1849534 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 718
2 个回答

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

宏有几个方面使它们“不好”(我稍后会扩展每个方面,并提出替代方案):

  1. 您不能调试宏。
  2. 宏扩展会导致奇怪的副作用。
  3. 宏没有“命名空间”,因此如果您的宏与其他地方使用的名称冲突,您会在不需要的地方获得宏替换,这通常会导致奇怪的错误消息。
  4. 宏可能会影响您没有意识到的事情。

所以让我们在这里稍微扩展一下:

1) 无法调试宏。 当您有一个转换为数字或字符串的宏时,源代码将具有宏名称,并且许多调试器无法“看到”宏转换为什么。所以你实际上并不知道发生了什么。

替换:使用 enumconst T

对于“类似函数”的宏,因为调试器在“每个源代码行”级别上工作,所以无论是一条语句还是一百条语句,您的宏都将像一条语句一样运行。很难弄清楚发生了什么。

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

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) 宏没有命名空间

如果我们有一个宏:

 #define begin() x = 0

我们在 C++ 中有一些使用 begin 的代码:

 std::vector<int> v;

... stuff is loaded into v ...

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

现在,你认为你得到了什么错误信息,你在哪里寻找错误[假设你完全忘记了——或者甚至不知道——存在于其他人编写的某个头文件中的 begin 宏? [如果你在 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();

}

现在,不看宏,你会认为 begin 是一个函数,它不应该影响 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 中的不同结果值。

原文由 Mats Petersson 发布,翻译遵循 CC BY-SA 4.0 许可协议

“宏是邪恶的”这句话通常是指使用#define,而不是#pragma。

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

  • 将幻数定义为宏

  • 使用宏替换表达式

这么多年后,有了新的 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);

在分配给 c 之后,您会期望 a 和 c 为 6(就像使用 std::max 而不是宏一样)。相反,代码执行:

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

最重要的是,宏不支持命名空间,这意味着在代码中定义宏将限制客户端代码可以使用的名称。

这意味着如果您定义了上面的宏(对于 max),您将不再能够在下面的任何代码中 #include <algorithm> ,除非您明确编写:

 #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;
}

这可以完成宏所做的所有事情,但有一个限制:如果参数的类型不同,则模板版本会强制您显式(这实际上会导致更安全、更显式的代码):

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

如果将此最大值定义为宏,则代码将编译(带有警告)。

如果此最大值被定义为模板函数,编译器将指出歧义,您必须说 max<int>(a, b)max<double>(a, b) (从而明确说明您的意图)。

原文由 utnapistim 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题