为什么其他编程语言不支持类似于C及其后代的预处理指令?

15


我想知道为什么其他编程语言不支持这个特性。我理解C/C++代码是平台相关的,所以要让它在各种平台上工作(编译和执行),需要使用预处理器指令。除此之外,还有许多其他用途。比如你可以把所有的调试printf放在#if DEBUG ... #endif中。因此,在进行发行版构建时,这些代码行不会被编译到二进制文件中。
但是在其他语言中,实现这一点(后半部分)很困难(或者可能是不可能的,我不确定)。所有的代码都会被编译进二进制文件中,增加了其大小。所以我的问题是,“为什么Java或其他现代编译语言不支持这种更方便地向二进制文件中包含或排除某些代码的特性?”


4
因为C和C++编译器是相当复杂的程序,包含了一半Common Lisp的特性,这些特性是临时指定、非正式说明、错误繁多、执行速度缓慢的。 - Artelius
4
使用C预处理器进行某些需要在其他编程语言中反射的花哨操作确实有一些明显的优势。然而,平台抽象和调试代码最好用其他方式处理。 - Chris Arguin
11个回答

10

大多数没有预处理器的主流编程语言通常有一种不同但更清晰的方式来实现相同的效果。

拥有像 cpp 这样的文本预处理器是一把双刃剑。由于 cpp 实际上并不“了解”C语言,它只是将文本转换为其他文本。这会造成许多维护问题。以 C++ 为例,其中许多预处理器的使用已明确被弃用,而更好的特性则包括:

  • 对于常量,使用 const 而不是 #define
  • 对于小函数,使用 inline 而不是 #define

C++ FAQ 称宏是邪恶的,并提供了多个避免使用宏的理由。


是的。我的主要关注点是“我是否必须依赖编译器足够聪明,能够执行所有优化操作,例如删除死代码等?” - bhups
@bhups:当然。你甚至可以依赖于C/C++编译器来删除死代码,而不会在代码中散布条件编译语句。 - Eli Bendersky
不要忘记在C++中使用模板作为宏的替代方案。 - Georg Fritzsche
-1 因为尽管链接对我来说甚至都无法工作,但该URL似乎指向某种私人封锁/论坛。如果存在一个与主要来源不同的原始来源,则仅使用随机人员作为索赔参考是毫无意义的。 - dhein
“...实现相同效果的方法。”很抱歉,这是错误的。其他语言通常不能做到相同的事情。有些要求需要预处理器,特别是在小型嵌入式系统上。另一种方法是编写128个不同版本的相同代码,这也是一个不好的想法。 - 12431234123412341234123

9
预处理器的可移植性优势远远被滥用的可能性所淹没。以下是我在工业界看到的一些实际代码示例:
- 函数体与 #ifdef 纠缠在一起,使得很难阅读函数并弄清楚正在发生什么。请记住,预处理器处理的是文本而不是语法,因此您可以做一些非常不符合语法的事情。 - 代码可能会在 #ifdef 的不同分支中重复出现,这使得维护有关正在发生的事情的单个真相点变得困难。 - 当应用程序面向多个平台时,编译所有代码变得非常困难,而不仅仅是为开发人员平台选择的任何代码。您可能需要设置多台机器。(例如,在BSD系统上设置准确模拟GNU标头的交叉编译环境是昂贵的。)在大多数Unix变体都是专有的,并且供应商必须支持它们所有的日子里,这个问题非常严重。今天,当许多版本的Unix是免费的时候,这个问题不那么严重了,尽管在Unix环境中复制本地Windows标头仍然非常具有挑战性。 - 有些代码受到如此多的 #ifdef 保护,以至于您无法弄清楚需要哪些 -D 选项组合才能选择该代码。这个问题是NP难的,因此已知的最佳解决方案需要尝试指数多种不同的定义组合。当然,这是不切实际的,因此真正的后果是逐渐填充未编译的代码系统。这个问题破坏了重构,当然这样的代码完全不受单元测试和回归测试的影响,除非您设置了一个庞大的多平台测试农场,即使那样也可能不行。 - 在现场,我看到这个问题导致情况变得很糟糕,例如重新设计的应用程序经过仔细测试和发布,只收到立即的错误报告,即该应用程序在其他平台上甚至无法编译。如果代码被 #ifdef 隐藏并且我们无法选择它,则无法保证它是否有类型检查或者甚至是否符合语法。
另一方面,更先进的语言和编程技术已经减少了在预处理器中进行条件编译的需要。
  • 对于一些语言,比如Java,所有与平台相关的代码都在JVM的实现和相关库中。人们已经付出巨大的努力制作了不依赖于平台的JVM和库。

  • 在许多语言中,如Haskell、Lua、Python、Ruby等,设计者尽力减少与C相比的与平台相关的代码量。

  • 在现代语言中,你可以将与平台相关的代码放在一个单独的编译单元中,在编译接口后面。许多现代编译器都有很好的功能,可以跨接口边界内联函数,这样你不需要为这种抽象付出太多(或任何)代价。而这在C中并不是这种情况,因为(a)没有单独的编译接口;单独的编译模型假设#include和预处理器;(b)C编译器在64K的代码空间和64K的数据空间的机器上成熟发展起来,一个足够复杂的编译器能够在模块边界内联是几乎不可想象的。今天这样的编译器是常规的。一些先进的编译器动态地内联和专门化方法。

摘要:通过使用语言机制而不是文本替换来隔离与平台相关的代码,您将所有代码暴露给编译器,至少会得到类型检查,并有机会执行诸如静态分析之类的操作,以确保适当的测试覆盖。你还排除了一整套导致代码不可读的编码实践。


6
由于现代编译器足够智能,可以在大多数情况下删除死代码,因此手动向编译器提供这种代码已不再必要。即,代替:
#include <iostream>

#define DEBUG

int main()
{
#ifdef DEBUG
        std::cout << "Debugging...";
#else
        std::cout << "Not debugging.";
#endif
}

您可以做以下事情:

#include <iostream>

const bool debugging = true;

int main()
{
    if (debugging)
    {
        std::cout << "Debugging...";
    }
    else
    {
        std::cout << "Not debugging.";
    }
}

你可能会得到相同或至少类似的代码输出。


编辑/注释:在C和C++中,我绝对不会这样做——如果没有其他选择,我会使用预处理器,因为这使得我的代码读者立刻清楚地知道某个部分不应该在某些条件下被编译。然而,我想说的是,这就是为什么许多语言避免使用预处理器的原因。


7
那么,我如何在运行时使用 if 语句添加数据和类型声明呢?你的方案不足以进行有趣的配置管理。 - Ira Baxter
5
我该如何在不编辑源文件的情况下,通过构建命令选项来改变行为? - Chris Dodd
@Ira && @Chris:我不是说我喜欢这个,但我是说这就是为令许多语言避免使用预处理器的原因。 - Billy ONeal
我希望能有一个更强大的CPP,但是我宁愿使用这个而不是依赖DCE。 - David X
问题在于,当某个函数仅在一个平台上可用时,即使您从未调用该函数,编译器也会在该函数不存在时发出警告。 - 12431234123412341234123

3
更好的问题是,为什么C语言要使用预处理器来实现这些元编程任务?这不是一种特性,而是对当时技术的妥协。
在C语言中,预处理指令是在机器资源(CPU速度、RAM)稀缺(且昂贵)的时代开发出来的。预处理器提供了一种在内存有限的慢速机器上实现这些功能的方法。例如,我拥有的第一台计算机只有56KB的RAM和2Mhz的CPU。它仍然有完整的K&R C编译器可用,虽然会耗尽系统的资源,但还是可以使用的。
更现代的语言利用今天更强大的机器来提供更好的处理元编程任务的方式,这些任务以前需要预处理器来处理。

2

其他语言通过使用通用的预处理器(如m4)来支持此功能。

我们真的希望每种语言都有自己的文本替换执行实现吗?


3
不行!你希望条件/宏以一种不需要你或分析工具来弄清楚含有半个关键字(或任何其他字符串处理后果)是如何引入的方式与语言相互作用。这意味着你的语言需要以一种可控、合理的方式包含这些功能。 - Ira Baxter
1
@Ira Baxter:文本预处理是存在的。它有其限制,我们都知道这一点。但它确实做到了它所说的。 - Borealid
1
你显然没有尝试构建一个针对C语言的静态分析工具,该工具在不扩展预处理器指令的情况下推理程序。您绝不想让图灵完备的字符串处理器站在您的静态分析和实际代码之间;如果您认为C预处理器很糟糕,那么等到您遇到某些程序员编写的M4因式分解程序,该程序根据答案有条件地将关键字粘合在一起,您就会更加感受到这种糟糕程度了。(C的条件语句已经够糟糕了)。此外,C预处理器确实只是像说明书上说的那样工作。但这并没有帮助。 - Ira Baxter
在使用宏(文本)处理器时并没有什么特别的。但是cpp更适合于C(C ++,Objective-C)或其他与它们共享某些词法规则的语言。我认为可以熟练地使用适当设置为该语言的通用预处理器,例如M4。 - ShinTakezou

2
请注意,宏/预处理/条件等通常被视为编译器/解释器功能,而不是语言功能,因为它们通常与正式的语言定义完全独立,并且对于相同语言的不同编译器实现可能会有所不同。
在许多语言中,条件编译指令比if-then-else运行时代码更好的情况是需要条件编译时语句(例如变量声明)。例如: $if debug array x $endif ... $if debug dump x $endif
只有在需要x时才声明/分配/编译x,而
array x boolean debug ... if debug then dump x
可能必须无论debug是否为真都要声明x。

2

C预处理器可以在任何文本文件上运行,不一定是C语言。

当然,如果在其他语言上运行,它可能会将其分词成奇怪的方式,但对于像 #ifdef DEBUG 这样的简单块结构,你可以将其放在任何语言中,在其上运行C预处理器,然后在其上运行特定于你所使用语言的编译器,这样就能够正常工作了。


4
只有包含类似于C语言标记的文件才能够合理地运行它(FORTRAN符合此测试;Python和APL则不符合)。 - Ira Baxter

2
许多现代编程语言实际上具有比CPP更强大的句法元编程能力。例如,几乎所有现代Lisp(如Arc、Clojure、Common Lisp、Scheme、newLISP、Qi、PLOT和MISC等)都拥有非常强大的(实际上是Turing完全的)宏系统,为什么要局限于不好用的CPP样式宏,这些甚至不是真正的宏,只是文本片段呢?其他具有强大句法元编程功能的语言包括Io、Ioke、Perl 6、OMeta和Converge。

1
对于那些提供类似C/C++裸机访问的语言中的元编程世界,我也会将D语言加入到列表中。 - dsimcha
CPP也是伪图灵完备的(伪是因为你有一些内存限制,就像每个现实世界的应用程序一样)。 - 12431234123412341234123
这些都是有趣的例子,但它并没有解释为什么更大、更主要的语言(例如JS、Java、Python、Ruby、Go、Dart等)缺少这样一个功能。 - Nico

0

另外一个没有人提到的问题是平台支持。

大多数现代语言无法在与 C 或 C++ 相同的平台上运行,也不打算在这些平台上运行。例如,Java、Python 和 C# 等本地编译语言需要堆栈,它们设计用于在具有内存管理、库和大量空间的操作系统上运行,它们无法在独立环境中运行。在这种情况下,你可以使用其他方法来实现相同的功能。C 可以用于编程具有 2KiB ROM 的控制器,但大多数应用程序需要预处理器。


0

其他编程语言也有更好的动态绑定。例如,我们有一些代码由于出口原因不能发送给某些客户。我们的“C”库使用#ifdef语句和复杂的Makefile技巧(基本上是相同的)。

Java代码使用插件(如Eclipse),这样我们就不会发送那段代码。

您可以通过使用共享库在C中执行相同的操作...但预处理器要简单得多。


1
你也可以在C中使用动态加载模块吗? - Georg Fritzsche
是的,通过使用共享库。严格来说,这不是 C 语言的一部分,但它是 POSIX 的一部分。 - Chris Arguin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接