为什么预处理器宏是邪恶的,还有哪些替代方法?


92

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

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

但是总的来说,宏及其用法的替代方案又如何呢?


19
#pragma不是宏。
FooF

1
@foof预处理程序指令?
user1849534

6
@ user1849534:是的,就是这样...有关宏的建议不在讨论中#pragma
Ben Voigt 2012年

1
你可以做很多事constexprinline函数和templates,但boost.preprocessorchaos表明宏有自己的位置。且不说配置宏差异的编译器,平台等
布兰登

Answers:


161

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

宏有一些方面使它们“不好”(我将在后面进行扩展,并提出替代方案):

  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宏?[并且如果您在包含之前包含该宏,则会更加有趣-您将陷入奇怪的错误中,这在您查看代码本身时完全没有意义。

替换:嗯,没有什么比“规则”更可替换的了-只对宏使用大写名称,而对其他东西则不使用所有大写名称。

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


6
这些if else问题可以通过将宏体包裹在里面来解决do { ... } while(0)。这表现为人们所期望的相对于iffor和其他潜在-风险控制流程的问题。但是可以,一个实函数通常是一个更好的解决方案。 #define macro(arg1) do { int x = func(arg1); func2(x0); } while(0)
亚伦·麦克戴德

11
@AaronMcDaid:是的,有些解决方法可以解决这些宏中暴露的一些问题。我的帖子的重点并不是要演示如何很好地执行宏,而是要指出“错误宏的难易程度”,那里有一个不错的选择。就是说,宏可以很容易地解决,有些时候宏也是正确的选择。
Mats Petersson

1
在第3点中,错误不再是真正的问题。像Clang这样的现代编译器会说类似的东西,note: expanded from macro 'begin'并显示在哪里begin定义。
kirbyfan64sos'3

5
宏很难翻译成其他语言。
Marco van de Voort,

1
@FrancescoDondi:stackoverflow.com/questions/4176328/...。(在这个问题的答案了不少了,它谈论我++ * i ++在和这样的
垫皮特森

21

俗称“宏是邪恶的”通常是指使用#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

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

这意味着,如果您在上方定义了宏(最大),则您将不再能够 #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)(并因此明确说明您的意图)。


1
它不一定是特定于c ++ 11的。您可以简单地使用函数来替换macros-as-expressions用法,并使用[static] const / constexpr来替换macros-as-constants用法。
utnapistim 2012年

1
甚至C99都允许使用const int someconstant = 437;,并且几乎可以使用所有使用宏的方式。小功能也是如此。在某些情况下,您可以将某些内容编写为无法在C中的正则表达式中工作的宏(您可以编写一些内容来对任何类型的数字数组求平均值,而C则不能这样做-但C ++具有模板为了那个原因)。尽管C ++ 11增加了一些其他内容,“您不需要宏”,但是大多数C / C ++已经解决了。
马特·彼得森

在传递参数的同时进行预递增是一种糟糕的编码实践。而任何人在C / C ++编码应该不会设定一个功能,如呼叫是不是宏。
StephenG

许多实现会自动在标识符后面加上括号maxmin如果后面有左括号,则会自动将其括起来。但是您不应该定义这样的宏...
LF

14

常见的麻烦是:

#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)

2
有趣的例子,基本上它们只是没有语义的标记
user1849534

是。它们被扩展为提供给宏的方式。DIV可以使用一对()重写该宏b
phaazon

2
您的意思是#define DIV(a,b),不是#define DIV (a,b),这是非常不同的。
rici 2012年

6
#define DIV(a,b) (a) / (b)还不够好 按照一般惯例,请始终添加最外面的括号,例如:#define DIV(a,b) ( (a) / (b) )
PJTraill

3

宏对于创建通用代码(宏的参数可以是任何东西),有时带有参数,特别有价值。

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

OTOH,可以通过以下方式获得类似的结果:

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

  • C ++中的模板(通用参数类型和值)

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

编辑:至于为什么宏是坏的:

1)不对参数进行类型检查(它们没有类型),因此很容易被滥用2)有时会扩展为非常复杂的代码,这在预处理文件中可能难以识别和理解3)易于出错宏中易于使用的代码,例如:

#define MULTIPLY(a,b) a*b

然后打电话

MULTIPLY(2+3,4+5)

扩大

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

要拥有后者,您应该定义:

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

3

我认为在调用预处理器定义或宏时没有任何问题。

它们是c / c ++中的一种(元)语言概念,并且像任何其他工具一样,如果您知道自己在做什么,它们可以使您的生活更轻松。宏的麻烦在于,它们会在您的c / c ++代码之前进行处理,并生成新的代码,这些代码可能会出错并导致编译器错误,而这些错误几乎是显而易见的。从好的方面来说,它们可以帮助您保持代码的清洁度,并且如果使用得当,可以节省很多键入时间,因此这取决于个人喜好。


另外,正如其他答案所指出的那样,设计不当的预处理器定义可能会生成具有有效语法但语义含义不同的代码,这意味着编译器不会抱怨,并且您在代码中引入了一个错误,而该错误将更加难以发现。
SandiHrvić'12

3

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

没有宏,这种功能很难实现。宏实际上是一个很棒的软件配置管理工具,而不仅仅是创建重复使用代码的快捷方式的一种方式。为宏中的可重用性而定义函数肯定会造成问题。


在编译过程中在cmdline上设置Macro值以从一个代码库构建两个变体确实很棒。适可而止。
kevinf '17

1
从某种角度看,这种用法是最危险的一种:工具(IDE,静态分析器,重构)将很难确定可能的代码路径。
erenon

1

我认为问题在于,编译器没有很好地优化宏,并且读取和调试它们是“丑陋的”。

通常,通用函数和/或内联函数是一个很好的选择。


2
是什么使您相信宏没有得到很好的优化?它们是简单的文本替换,其结果与不使用宏编写的代码一样多。
Ben Voigt 2012年

@BenVoigt,但是他们不考虑语义,这可能会导致某些事情可以被认为是“非最佳”的……至少这是我对此stackoverflow.com/a/14041502/1849534的
user1849534

1
@ user1849534:在编译上下文中,“优化”一词并不是什么意思。
Ben Voigt 2012年

1
@BenVoigt确实,宏只是文本替换。编译器只是复制代码,这不是性能问题,但会增加程序大小。在某些程序大小受限制的情况下尤其如此。某些代码的宏内容非常丰富,以至于程序的大小是原来的两倍。
Davide Icardi 2012年

1

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

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

替代方案- 出于类似目的,可以使用ini,xml,json格式的某种配置文件。但是使用它们将对预处理器宏可以避免的代码产生运行时影响。


1
因为C ++ 17 constexpr if +包含“ config” constexpr变量的头文件可以替换#ifdef。
Enhex
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.