为什么大多数现代编程语言中没有包含宏?


31

我知道在C / C ++中极其不安全地实现它们。他们不能以更安全的方式实施吗?宏的缺点真的严重到足以超过它们提供的强大功能吗?


4
宏究竟提供了什么功能,而其他方法无法轻松实现?
Chinmay Kanchi 2010年

2
在C#中,它采用了一种核心语言扩展来将创建一个简单的属性和后备字段包装到一个声明中。这不仅需要词汇宏:还需要如何创建与Visual Basic WithEvents修饰符相对应的功能?您需要像语义宏之类的东西。
杰弗里·汉汀

3
宏的问题在于,像所有强大的机制一样,它使程序员可以打破其他人已经或将要做出的假设。假设是推理的关键,而没有能力对逻辑进行可行的推理,则进展变得令人望而却步。
dan_waterworth 2012年

2
@Chinmay:宏生成代码。Java语言没有这种功能。
凯文·克莱恩

1
@Chinmay Kanchi:宏允许在编译时而不是在运行时执行(评估)代码。
Giorgio 2013年

Answers:


57

我认为主要原因是宏是词法的。这有几个后果:

  • 编译器无法检查宏是否在语义上是关闭的,即,它像函数一样代表“含义单元”。(考虑#define TWO 1+1- TWO*TWO等于多少?3.)

  • 宏的输入方式与函数不同。编译器无法检查参数和返回类型是否有意义。它只能检查使用宏的扩展表达式。

  • 如果代码无法编译,则编译器无法知道错误是在宏本身中还是在使用宏的地方。编译器要么在一半时间报告错误的位置,要么必须报告两者,即使其中之一可能很好。(考虑#define min(x,y) (((x)<(y))?(x):(y)):如果xy不匹配,或者没有实现operator<?)

  • 自动化工具无法以语义上有用的方式使用它们。尤其是,对于像宏一样工作的函数,您不能拥有像IntelliSense这样的东西,但可以扩展为表达式。(同样是min示例。)

  • 宏的副作用不像函数那样明显,这可能会引起程序员的困惑。(再次考虑min示例:在函数调用中,您知道x只被计算一次,但是在这里,如果不查看宏就无法知道。)

就像我说的,这些都是宏是词法事实的结果。当您尝试将它们变成更合适的东西时,最终会得到函数和常量。


32
并非所有的宏语言都是词汇。例如,Scheme宏是语法,C ++模板是语义。词法宏系统的区别在于,它们可以在不了解其语法的情况下被附加到任何语言上。
杰弗里·汉汀

8
@Jeffrey:我想我的“宏是词法的”确实是“当人们通常以编程语言来引用宏时,他们会想到词法宏”的简写。不幸的是,Scheme会将此加载的术语用于根本不同的事物。但是,C ++模板并没有被广泛地称为宏,大概正是因为它们并不完全是词法。
Timwi

25
我认为Scheme对宏一词的使用可以追溯到Lisp,这可能意味着它早于大多数其他用途。IIRC C / C ++系统最初称为预处理器
贝文

16
@贝文是正确的。说宏是词汇,就像说鸟不会飞,因为您最熟悉的鸟是企鹅。也就是说,您提出的大部分(但不是全部)要点也适用于语法宏,尽管程度可能较小。
Laurence Gonsalves

13

但是是的,宏可以进行设计,比C / C ++更好的实现。

宏的问题在于它们实际上是一种语言语法扩展机制,可以将您的代码重写为其他形式。

  • 在C / C ++情况下,没有基本的健全性检查。如果小心,一切都会好起来的。如果您犯了一个错误,或者您过度使用了宏,则可能会遇到大问题。

    除此之外,您可以使用(C / C ++样式)宏执行许多简单操作,而这些操作可以通过其他语言以其他方式完成。

  • 在诸如各种Lisp方言之类的其他语言中,宏可以更好地与核心语言语法集成在一起,但是您仍然可以在宏“泄漏”中进行声明时遇到问题。这可以通过卫生宏解决


简要的历史背景

宏(宏指令的缩写)首先出现在汇编语言的上下文中。根据Wikipedia的说法,1950年代某些IBM汇编器中提供了宏。

最初的LISP没有宏,但是它们最早是在1960年代中期引入MacLisp的:https : //stackoverflow.com/questions/3065606/when-did-the-idea-of-macros-user-defined-code -transformation-appearhttp://www.csee.umbc.edu/courses/331/resources/papers/Evolution-of-Lisp.pdf。在此之前,“ fexprs”提供了类似于宏的功能。

最早的C版本没有宏(http://cm.bell-labs.com/cm/cs/who/dmr/chist.html)。这些是通过预处理器在1972-73年左右添加的。在此之前,C仅支持#include#define

M4宏预处理器起源于1977年左右。

显然,较新的语言在操作模型是句法而非文本的地方实现了宏。

因此,当有人谈论术语“宏”的特定定义的首要性时,重要的是要注意该含义已经随着时间而演变。


9
C ++具有两个宏系统(B):预处理器和模板扩展。
杰弗里·汉汀

我认为声称模板扩展是一个宏正在扩展宏的定义。使用您的定义使原始问题变得毫无意义,而这完全是错误的,因为实际上大多数现代语言都具有宏(根据您的定义)。但是,将宏用作绝大多数开发人员会使用它,这是一个很好的问题。
Dunk

@Dunk,如Lisp中对宏的定义早于可悲的C预处理器中对“现代”愚蠢的理解。
SK-logic

@SK:“技术上”正确并掩盖对话更好还是更好地被理解?
Dunk

1
@Dunk,术语不属于无知的部落。永远不要考虑他们的无知,否则将导致愚弄整个计算机科学领域。如果有人仅将“宏”理解为对C预处理程序的引用,那无非就是无知。我怀疑有很大一部分人从未听说过Lisp,甚至从未听说过Office中的VBA“宏”(pardonnez monfrançais)。
SK-logic

12

正如Scott所指出的,宏可以让您隐藏逻辑。当然,函数,类,库和许多其他常见设备也是如此。

但是强大的宏系统可以走得更远,使您能够设计和利用通常在该语言中找不到的语法和结构。实际上,这可能是一个很棒的工具:特定领域的语言,代码生成器等等,所有这些都可以在一种语言环境下轻松实现。

但是,它可能会被滥用。它会使代码更难于阅读,理解和调试,增加新程序员熟悉代码库所需的时间,并导致代价高昂的错误和延迟。

因此,对于旨在简化编程的语言(例如Java或Python),这样的系统简直是天灾人祸。


3
然后,Java通过添加foreach,注释,断言,泛型逐个删除而脱离了轨道……
Jeffrey Hantin 2010年

2
@Jeffrey:我们不要忘记,有很多第三方代码生成器。再次考虑,让我们忘记那些。
Shog9

4
Java如何简化而不是简单的另一个示例。
杰弗里·汉汀

3
@JeffreyHantin:foreach有什么问题?
Casebash 2013年

1
@Dunk这个类比可能是一个延伸……但是我认为语言的可扩展性有点像p。实施成本高昂,很难加工成所需的形状,如果使用不当,则非常危险,但如果使用得当,其功能也非常强大。
杰弗里·汉汀

6

在某些情况下,可以非常安全地实现宏-例如,在Lisp中,宏只是将转换后的代码作为数据结构(s表达式)返回的函数。当然,Lisp的优点在于它具有同调性和“代码就是数据”这一事实。

这个Clojure示例是一个简单的宏示例,它指定在异常情况下要使用的默认值:

(defmacro on-error [default-value code]
  `(try ~code (catch Exception ~'e ~default-value)))

(on-error 0 (+ nil nil))               ;; would normally throw NullPointerException
=> 0                                   ;l; but we get the default value

即使在Lisps中,一般建议是“除非必须,否则不要使用宏”。

如果您不使用谐音语言,则宏会变得更加棘手,并且其他各种选项都有一些陷阱:

  • 基于文本的宏(例如C预处理器)易于实现,但要正确使用非常棘手,因为您需要以文本形式生成正确的源语法,包括任何语法上的怪癖
  • 基于宏的DSLS-例如C ++模板系统。复杂本身会导致一些棘手的语法,对于编译器和工具编写者来说,正确处理可能会非常复杂,因为它给语言语法和语义带来了重大的新复杂性。
  • AST /字节码操作API(例如Java反射/字节码生成)从理论上讲非常灵活,但会变得很冗长:它可能需要很多代码才能完成非常简单的事情。如果要用十行代码来生成等效于三行函数的代码,那么元编程的努力并没有带来多少好处。

此外,宏可以做的所有事情最终都可以以某种完整的语言以其他方式实现(即使这意味着要编写很多样板)。由于所有这些棘手的结果,因此许多语言都认为宏并不真正值得花所有的精力来实现,这并不奇怪。


啊。没有更多的“代码是数据是一件好事”的垃圾了。代码就是代码,数据就是数据,并且未能正确地将两者隔离是一个安全漏洞。 您可能会认为,SQL注入作为现有最大的漏洞类别之一的出现,将使使代码和数据易于一劳永逸地互换的想法蒙羞。
梅森惠勒2013年

10
@Mason-我认为您不了解代码即数据的概念。所有C程序源代码也是数据-它恰好以文本格式表示。Lisps是相同的,除了它们以实用的中间数据结构(s表达式)表达代码外,使它们能够在编译之前由宏进行操纵和转换。在这两种情况下,向编译器发送不受信任的输入都是一个安全漏洞-但这很难做到,这是您做一些愚蠢的事情而不是编译器的错。
mikera 2013年

Rust是另一个有趣的安全宏系统。它具有严格的规则/语义,避免了与C / C ++词法宏相关的问题,并使用DSL将输入表达式的部分捕获为宏变量,以便稍后插入。它不像Lisps调用输入上的任何函数那样强大,但是它确实提供了大量有用的操作宏,可以从其他宏调用这些宏。
zstewart

啊。没有更多的安全垃圾。否则,应将每个系统都内置冯·诺依曼体系结构归咎于每个系统,例如,每个具有自修改代码功能的IA-32处理器。我怀疑您是否可以在物理上填补漏洞……无论如何,您必须面对一个普遍的事实:代码和数据之间的同构在许多方面是世界的本质。而且,(可能)您(程序员)有责任保持安全要求的不变性,这与在任何地方人为地应用过早隔离都是不同的。
FrankHB

4

要回答您的问题,请考虑主要用于什么宏(警告:大脑编译的代码)。

  • 用于定义符号常量的宏 #define X 100

可以很容易地替换为: const int X = 100;

  • 用于定义(基本上)内联类型无关功能的宏 #define max(X,Y) (X>Y?X:Y)

在任何支持函数重载的语言中,都可以通过具有正确类型的重载函数,或者以支持泛型的语言,通过泛型函数,以更加类型安全的方式进行模拟。该宏将很乐意尝试比较可能编译的任何内容,包括指针或字符串,但几乎可以肯定不是您想要的。另一方面,如果使宏成为类型安全的宏,则与重载函数相比,它们没有任何好处或便利。

  • 用于指定常用元素快捷方式的宏。 #define p printf

这很容易被功能p()相同的函数所替代。这在C中很复杂(要求您使用va_arg()函数族),但是在许多其他支持可变数量的函数参数的语言中,它要简单得多。

支持这些功能中的一种语言,而不是一个特殊的宏语言更简单,更容易出错,远不如混乱给他人阅读的代码。实际上,我无法想到无法以其他方式轻松复制的宏的单个用例。宏真正有用的唯一地方是当它们与条件编译结构#if(如等)绑定在一起时。

关于这一点,我不会与您争论,因为我认为流行语言中的条件编译的非预处理器解决方案非常麻烦(例如Java中的字节码注入)。但是,像D这样的语言提出了不需要预处理器的解决方案,并且比使用预处理器条件更麻烦,同时不容易出错。


1
如果必须#define max,请在参数两边加上方括号,这样就不会产生来自运算符优先级的意外影响...例如#define max(X,Y)((X)>(Y)?(X):( Y))
foo

您的确意识到这只是一个例子...目的是为了说明。
2011年

对于此系统处理问题,+ 1。我想补充一下,条件编译很容易成为噩梦-我记得有一个名为“ unifdef”(??)的程序,其目的是使其在后期处理后仍然可见。
Ingo

7
In fact, I can't think of a single use-case for macros that can't easily be duplicated in another way:至少在C语言中,如果不使用宏,就不能使用令牌级联来形成标识符(变量名)。
Charles Salvia 2012年

宏使确保某些代码和数据结构保持“并行”成为可能。例如,如果带有关联消息的条件数量很少,并且需要将其以简明的格式保存,那么尝试使用enum来定义条件并使用常量字符串数组来定义消息可能会导致问题。枚举和数组不同步。使用宏定义所有(枚举,字符串)对,然后每次两次将该宏与范围内的其他正确定义一起使用,将使每个枚举值都放在其字符串旁边。
2012年

2

我在宏中看到的最大问题是,当宏大量使用时,它们会使代码难以阅读和维护,因为它们使您可以在宏中隐藏逻辑,这些逻辑可能难于查找,也可能难于查找。 )。


不应记录宏吗?
Casebash

@Casebash:当然,宏应该像其他任何源代码一样记录在文档中...但是在实践中,我很少看到它完成。
Scott Dorman 2010年

3
曾经调试过文档吗?当处理糟糕的代码时,这还远远不够。
JeffO 2010年

OOP也有一些这样的问题……
aoeu256

2

最好不要指出C / C ++中的MACRO非常有限,容易出错并且没有那么有用。

用LISP或z / OS汇编程序语言实现的MACRO是可靠且非常有用的。

但是由于滥用了C语言中有限的功能,因此它们赢得了不好的声誉。因此,没有人再实现宏了,相反,您会得到诸如Templates之类的东西,这些东西可以执行一些以前使用的简单填充宏,而您得到的Java之类的注解则可以完成一些过去使用的更复杂的东东。

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.