为啥预处理器宏是邪恶的,有啥替代方案?
Posted
技术标签:
【中文标题】为啥预处理器宏是邪恶的,有啥替代方案?【英文标题】:Why are preprocessor macros evil and what are the alternatives?为什么预处理器宏是邪恶的,有什么替代方案? 【发布时间】:2012-12-12 01:30:18 【问题描述】:我一直问这个问题,但我从来没有得到一个很好的答案;我认为几乎所有程序员在写第一个“Hello World”之前都遇到过“永远不应该使用宏”、“宏是邪恶的”之类的短语,我的问题是:为什么?这么多年了,新的 C++11 真的有替代品吗?
简单的部分是关于像 #pragma
这样的宏,它们是特定于平台和编译器的,并且大多数时候它们有像 #pragma once
这样的严重缺陷,这在至少 2 个重要情况下容易出错:同名不同路径以及一些网络设置和文件系统。
但总的来说,宏及其用法的替代品呢?
【问题讨论】:
#pragma
不是宏。
@foof 预处理指令?
@user1849534:是的,就是这样......关于宏的建议不是在谈论#pragma
。
你可以用constexpr
、inline
函数和templates
做很多事情,但是boost.preprocessor
和chaos
表明宏有它们的位置。更不用说针对不同编译器、平台等的配置宏了。
另见"Are all macros evil?"
【参考方案1】:
宏就像任何其他工具一样 - 用于谋杀的锤子并不邪恶,因为它是一把锤子。人们以这种方式使用它的方式是邪恶的。如果你想钉钉子,锤子是一个完美的工具。
宏有几个方面使它们“不好”(我稍后会详细介绍每个方面,并提出替代方案):
-
您不能调试宏。
宏扩展会导致奇怪的副作用。
宏没有“命名空间”,因此如果您的宏与其他地方使用的名称发生冲突,您会在不需要的地方得到宏替换,这通常会导致奇怪的错误消息。
宏可能会影响您没有意识到的事情。
所以让我们在这里稍微扩展一下:
1) 无法调试宏。 当您有一个转换为数字或字符串的宏时,源代码将具有宏名称,并且许多调试器无法“看到”宏转换为什么。所以你实际上并不知道发生了什么。
替换:使用enum
或const 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
我们有一些使用 begin 的 C++ 代码:
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
中的不同结果值。
【讨论】:
if else
的问题可以通过将宏体包裹在do ... while(0)
中来解决。对于if
和for
以及其他具有潜在风险的控制流问题,这与人们预期的一样。但是,是的,真正的功能通常是更好的解决方案。 #define macro(arg1) do int x = func(arg1); func2(x0); while(0)
@AaronMcDaid:是的,有一些变通方法可以解决这些宏中暴露的一些问题。我这篇文章的重点不是展示如何做好宏,而是展示“弄错宏是多么容易”,这里有一个很好的选择。也就是说,有些事情宏很容易解决,而且有时宏也是正确的做法。
在第 3 点中,错误不再是真正的问题。诸如 Clang 之类的现代编译器会说出类似 note: expanded from macro 'begin'
的内容,并显示 begin
的定义位置。
宏很难翻译成其他语言。
@FrancescoDondi:***.com/questions/4176328/…(这个答案有点低调,它谈到了 i++ * i++ 等。【参考方案2】:
“宏是邪恶的”这句话通常是指使用#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);
你会期望 a 和 c 在赋值给 c 之后为 6(就像使用 std::max 而不是宏一样)。相反,代码执行:
int c = ( ((++a) ? (b)) ? (++a) : (b) ); // after this, c = a = 7
除此之外,宏不支持命名空间,这意味着在代码中定义宏将限制客户端代码可以使用的名称。
这意味着如果你定义了上面的宏(for 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)
(从而明确说明你的意图)。
【讨论】:
不一定是 c++11 特定的;您可以简单地使用函数来替换宏作为表达式的用法和 [静态] const / constexpr 来替换宏作为常量的用法。 甚至 C99 也允许使用const int someconstant = 437;
,它几乎可以用于宏的所有使用方式。对于小功能也是如此。在某些情况下,您可以将某些东西写成不能在 C 中的正则表达式中工作的宏(您可以制作一些对任何类型数字的数组进行平均的东西,这是 C 做不到的 - 但 C++ 有模板为了那个原因)。虽然 C++11 添加了一些“你不需要为此使用宏”的东西,但它大部分已经在早期的 C/C++ 中解决了。
在传递参数的同时进行预增量是一种糟糕的编码习惯。任何使用 C/C++ 编码的人都应该不假定类似函数的调用不是宏。
如果标识符max
和min
后跟左括号,则许多实现会自愿将它们括起来。但是你不应该定义这样的宏...【参考方案3】:
一个常见的问题是:
#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)
【讨论】:
有趣的例子,基本上它们只是没有语义的标记 是的。它们以赋予宏的方式扩展。DIV
宏可以用 b
周围的一对 () 重写。
你的意思是#define DIV(a,b)
,而不是#define DIV (a,b)
,这很不一样。
#define DIV(a,b) (a) / (b)
不够好;作为一般做法,请始终添加最外面的括号,如下所示:#define DIV(a,b) ( (a) / (b) )
【参考方案4】:
宏对于创建通用代码(宏的参数可以是任何东西)尤其有用,有时带有参数。
更多,这段代码被放置(即插入)在宏被使用的地方。
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))
【讨论】:
【参考方案5】:我认为使用预处理器定义或您所称的宏没有任何问题。
它们是 c/c++ 中的(元)语言概念,与任何其他工具一样,如果您知道自己在做什么,它们可以让您的生活更轻松。宏的问题在于它们在您的 c/c++ 代码之前处理并生成可能有错误的新代码并导致编译器错误,这些错误几乎是显而易见的。从好的方面来说,如果使用得当,它们可以帮助您保持代码整洁并节省大量打字时间,因此这取决于个人喜好。
【讨论】:
另外,正如其他答案所指出的那样,设计不佳的预处理器定义可以生成具有有效语法但语义不同的代码,这意味着编译器不会抱怨,并且您在代码中引入了一个错误更难找到。【参考方案6】: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 上设置宏值以从一个代码库构建两个变体非常好。适度。 从某种角度来看,这种用法是最危险的:工具(IDE、静态分析器、重构)将很难找出可能的代码路径。【参考方案7】:预处理器宏在用于以下预期目的时并不邪恶:
使用#ifdef 类型的构造创建同一软件的不同版本,例如不同区域的窗口版本。 用于定义代码测试相关值。替代方案- 出于类似目的,可以使用某种 ini、xml、json 格式的配置文件。但是使用它们会对预处理器宏可以避免的代码产生运行时影响。
【讨论】:
自C++17 constexpr if + 包含“config” constexpr 变量的头文件可以替换#ifdef 的。【参考方案8】:根据我的经验,宏对于程序大小并不理想,并且可能难以调试。但如果小心使用,它们是可以的。
通用函数和/或内联函数通常是一个不错的替代方案。
【讨论】:
是什么让您相信宏没有得到很好的优化?它们是简单的文本替换,其结果与不使用宏编写的代码一样得到优化。 @BenVoigt 但他们不考虑语义,这可能会导致一些可能被认为是“非最佳”的事情......至少这是我对 ***.com/a/14041502/1849534 @user1849534:这不是“优化”这个词在编译上下文中的含义。 @BenVoigt 没错,宏只是文本替换。编译器只是复制代码,这不是性能问题,但会增加程序大小。在您有程序大小限制的某些情况下尤其如此。有些代码充满了宏,以至于程序的大小是原来的两倍。以上是关于为啥预处理器宏是邪恶的,有啥替代方案?的主要内容,如果未能解决你的问题,请参考以下文章
#if 和 #ifdef Objective-C 预处理器宏有啥区别?