现代语言中的预处理器是否已过时?

时间:2010-05-30 21:38:42

标签: language-agnostic preprocessor language-design

我正在为一个简单的宠物语言编写一个简单的编译器,我正在创建并来自C背景(虽然我用Ruby编写)我想知道是否需要预处理器。

你怎么看?现代语言中仍然需要“哑”预处理器吗? C#的条件编译功能会被视为“预处理器”吗?是否每种不包含预处理器的现代语言都具有正确替换它所需的实用程序? (例如,由于模板,C ++预处理器现在大部分已经过时(但仍然依赖)。)

9 个答案:

答案 0 :(得分:8)

C的预处理可以做一些非常简洁的事情,但是如果你看一下它所使用的东西,你就会意识到它通常只是为了增加另一层次的抽象。

  • 对不同平台上的不同操作进行预处理?它基本上是平台独立性的抽象层。
  • 预处理以轻松添加复杂代码?抽象,因为语言不够通用。
  • 在代码中添加扩展程序的预处理?抽象,因为你的代码/你的语言不够灵活。

所以我的答案是:如果您的语言足够高,则不需要预处理器 *。我不会把预处理称为邪恶或无用,我只是说语言越抽象,我就越不能想到它需要预处理。

*什么是高级别的?当然,这完全是主观的。

编辑:当然,我只是指。使用预处理器与其他代码文件连接或定义常量 邪恶。

答案 1 :(得分:7)

预处理器是一种廉价的方法,可以以丑陋的方式为语言提供不完整的元编程功能。

更喜欢真正的元编程或Lisp风格的宏。

答案 2 :(得分:5)

预处理器不是必需的。对于真正的元编程,你应该有类似MetaML或模板Haskell或卫生宏的方案。对于快速和肮脏的东西,如果您的用户绝对必须拥有它,那么总是m4

但是,现代语言应该支持相当于C的#line指令。这样的指令使编译器能够在原始源中定位错误,即使该源嵌入在解析器中也是如此发电机或词法生成器或文学程序。换句话说,

  • 设计您的语言,以便不需要预处理器。
  • 请勿将您的语言与祝福的预处理器捆绑在一起。
  • 但如果其他人有自己的理由使用预处理器(解析器生成很受欢迎),请提供对准确错误消息的支持。

答案 3 :(得分:2)

我认为预处理器是保持语言表达能力差的语言的拐杖。

我已经看到了很多滥用预处理器的东西,我很讨厌它们。

答案 4 :(得分:0)

预处理器是编译的分离阶段。 虽然预处理在某些情况下很有用,但它可能导致的麻烦和错误使它成为一个问题。

在C中,预处理器主要用于:

  1. 包含数据 - 虽然功能强大,但最常见的用例并不需要这样的功能,而“导入”/“使用”的东西(比如Java / C#)使用起来要干净得多,很少有人需要剩下的情况;
  2. 定义常量 - 为什么不提供“const”语句
  3. 宏 - 虽然C风格的宏非常强大(它们可以包含诸如返回之类的语句),但它们也会损害可读性。泛型/模板更清晰,虽然在某些方面不那么强大,但它们更容易理解。
  4. 条件编译 - 这可能是预处理器最合理的用例,但再一次让它的可读性变得很痛苦。在特定于平台的源代码中分离特定于平台的代码并使用常见的if语句最终会提高可读性。
  5. 所以我的答案是虽然功能强大,但预处理器会损害可读性和/或不是处理某些问题的最佳方法。较新的语言倾向于认为代码维护非常重要,并且出于这些原因,预处理器似乎已经过时了。

答案 5 :(得分:0)

这是您的语言,因此您可以在语言本身中构建您想要的任何功能,而无需预处理器。我不认为预处理器是必要的,它会在语言之上增加一层复杂性和模糊性。大多数现代语言都没有预处理器,而在C ++中,只有在没有其他选择时才使用它。

顺便说一句,我相信D在没有预处理器的情况下处理条件编译。

答案 6 :(得分:0)

这取决于您提供的其他功能。例如,如果我有一个const int N,你能为我提供N个变量吗?有N个成员变量,拿一个参数来构造所有这些变量吗?创建N个功能?执行不一定在循环中工作的N个操作(例如,传递N个参数)? N个模板参数?条件编译?不是整数的常数?

C预处理器在适当的手中是如此荒谬,你需要制作一种非常强大的语言,而不是一个人。

答案 7 :(得分:0)

我想说的是,虽然您应该避免使用预处理器来处理通常所做的大多数事情,但仍然需要它。

例如,在C ++中,为了编写像Catch这样的单元测试库,预处理器是绝对必要的。他们以两种不同的方式使用它:一个用于断言扩展 1 ,另一个用于测试用例 2 中的嵌套部分。

但是,预处理器不应该被滥用来在C ++中进行编译时计算,其中可以使用const表达式和模板元编程。

对不起,我没有足够的声誉来发布两个以上的链接,所以我把它放在这里:

  1. github.com/philsquared/Catch/blob/master/docs/assertions.md
  2. github.com/philsquared/Catch/blob/master/docs/test-cases-and-sections.md

答案 8 :(得分:0)

其他人指出,C 预处理器提供的大部分功能是为了弥补 C 语言的局限性。例如,由于缺少 #include 语句而存在 import 和包含保护,而由于缺少内联函数和常量声明而存在大量宏。

然而,在更现代的语言中仍然有益的 C 预处理器的一个特性是 #line 指令,因为它支持使用语义丰富的预处理器/编译器。一个例子,考虑yacc,它是一种域特定语言 (DSL),用于将解析器编写为 BNF 语法规则的集合。 yacc 的一个核心特性是称为 actions 的 C 代码块可以嵌入到 BNF 规则中。当使用 BNF 规则解析输入文件的一部分时,将执行嵌入在该规则中的操作。 yacc 编译器生成一个 C 文件,该文件实现了输入文件中指定的基于 BNF 的解析器,并将输入 Yacc 文件中出现的任何动作复制到生成的 C 文件中,但每个动作都被 {{ 1}} 指令。使用 #line 指令提供了两个重要的好处。

首先,如果操作中存在语法错误,那么 C 编译器生成的错误消息可以指定错误发生在 #line 而不是 <input-file-to-yacc>, line 42

其次,<output-file-generated-by-yacc>.c, line 3967 指令提供的位置信息被复制到由 C 编译器创建的生成的目标代码文件中。因此,如果您正在使用调试器来调查程序崩溃,如果导致崩溃的错误源自嵌入在 Yacc 输入文件中的操作,那么调试器将报告该错误源代码行的位置在 {{ 1}} 而不是 #line

C# 和 Perl 的设计者明智地提供了一个 <input-file-to-yacc>, line 42 指令。不幸的是,许多其他语言(Java 就是其中一种)的设计者忽略了提供 <output-file-generated-by-yacc>.c, line 3967 指令。因此,许多语言的类似 Yacc 的解析器生成器无法将嵌入操作的源位置传达给编译器(因此也无法传达给调试器)。