C99预处理器Turing是否已完成?


73

在发现Boost预处理器的功能之后,我发现自己想知道:C99预处理器Turing是否完整?

如果没有,那么缺少资格的原因是什么?


7
CPP中追求完整性的缺失本质上是递归,因为如果没有它,它就无法循环(并且由于您无法有条件地扩展宏的一部分,所以它实际上具有相当有限的条件)
Spudd86

Answers:


34

是滥用预处理器来实现图灵机的示例。请注意,需要一个外部构建脚本来将预处理器的输出反馈回其输入,因此预处理器本身并不完整。不过,这是一个有趣的项目。

从先前链接的项目的描述中:

预处理程序尚未完成图灵处理,至少在程序仅被处理过一次的情况下至少不会完成。即使允许程序包含自身,也是如此。(原因是对于给定的程序,预处理器只有有限数量的状态,加上一个堆栈,其中包括文件所在的位置。这只是下推式自动机。)

Paul Fultz II的答案令人印象深刻,而且肯定比我以为预处理器所能达到的范围更近,但这不是真正的图灵机。C预处理器有一定的限制,即使您有无限的内存和时间,它也无法像Turing机器一样执行任意程序。C规范的5.2.4.1节为C编译器提供了以下最低限制:

  • 完整表达式中带括号的表达式的63个嵌套级别
  • 内部标识符或宏名称中的63个有效初始字符
  • 在一个预处理翻译单元中同时定义4095个宏标识符
  • 逻辑源代码行中的4095个字符

下面的计数器机制要求每个值都有一个宏定义,因此宏定义限制将限制您可以循环多少次(EVAL(REPEAT(4100, M, ~))将产生未定义的行为)。这从本质上限制了您可以执行的程序的复杂性。多级扩展的嵌套和复杂性也可能会达到其他限制之一。

这与“无限内存”限制根本不同。在这种情况下,规范特别指出,即使具有无限的时间,内存等,也只需要符合标准的C编译器即可满足这些限制。任何超出这些限制的输入文件都可以以不可预测或不确定的方式处理(或直接拒绝)。某些实现可能有更高的限制,或者根本没有限制,但这被认为是“特定于实现的”,而不是标准的一部分。可能可以使用Paul Fultz II的方法在计算机上实现图灵机某些特定的编译器实现没有任何限制,但是从一般意义上说“可以在任何任意的,符合标准的C99预处理器上完成”,答案是否定的。由于这里的限制是语言本身所固有的,而不仅仅是我们无法构建无限计算机的副作用,所以我说这破坏了图灵的完整性。


12
这个答案是错误的,因为它下面的77点答案广泛显示。请不接受它,并接受更有用的答案,谢谢。
2015年

如果您的意思是下面的Paul Fultz II给出的115分的答案:它确认了这个答案。
reinierpost

1
限制在于语言本身,但不是出于规范,而是因为我们必须编写扫描来评估语言本身中的算法,没有机制可以应用无限次的扫描。
Paul Fultz II

141

宏不会直接递归扩展,但是有一些方法可以解决此问题。

在预处理器中进行递归的最简单方法是使用延迟表达式。延迟表达式是需要更多扫描才能完全扩展的表达式:

为什么这很重要?当宏被扫描并扩展时,它会创建一个禁用上下文。此禁用上下文将导致一个标记,该标记表示当前正在扩展的宏,显示为蓝色。因此,一旦将其涂成蓝色,宏将不再扩展。这就是为什么宏不递归扩展的原因。但是,禁用上下文仅在一次扫描期间存在,因此通过延迟扩展,我们可以防止宏变成蓝色。我们只需要对表达式进行更多扫描即可。我们可以用这个EVAL宏:

现在,如果我们要实施 REPEAT使用递归宏,则首先需要一些递增和递减运算符来处理状态:

接下来,我们需要更多的宏来执行逻辑:

现在,使用所有这些宏,我们可以编写一个递归REPEAT宏。我们使用REPEAT_INDIRECT宏来递归地引用自身。这可以防止宏被涂成蓝色,因为它将在不同的扫描(和使用不同的禁用上下文)下扩展。我们OBSTRUCT在这里使用,这将延迟两次扩展。这是必要的,因为条件WHEN应用一次扫描。

现在,由于计数器的限制,此示例仅限于10次重复。就像计算机中的重复计数器会受到有限内存的限制。就像在计算机中一样,可以将多个重复计数器组合在一起以解决此限制。此外,我们可以定义一个FOREVER宏:

这将尝试?永远输出,但最终将停止,因为不再应用任何扫描。现在的问题是,如果我们对其进行无限次扫描,该算法是否会完成?这被称为暂停问题,图灵完整性是证明暂停问题的不确定性所必需的。正如您所看到的,预处理器可以充当图灵完整的语言,但是它不仅限于计算机的有限内存,还受到应用的有限扫描次数的限制。


7
...哇。非常令人印象深刻!在这里,我认为C99预处理器显然还没有完全完成。.+1为开箱即用的思考
Earlz

+1非常有创意的方式来显示预处理器可以扫描磁带上的符号;-)(感谢mod接受标记来删除Wiki!)。
PP

我喜欢它如何使用O(log(N))宏递归N次。这比使用O(N)宏的Boost Preprocessor更好。
qbt937 '16

5
扫描的有限数量的类似于是类似于具有有限存储器的计算机。Turing完整性的本质是,即使实际上是在容量有限的机器上运行,对计算的说明本身也没有任何限制。
reinierpost

1
@Paul Fultz II这是因为您知道该函数将要完成的先验知识,因此证明了这一点。因此,您需要有限的时间才能完成。mu递归函数,例如搜索任意方程的解,不能由cpp计算。我说过cpp无法计算mu递归的类,关于您已经知道很多事情的函数的一些特定示例也不是。
alinsoar

5

为了使Turing完整,需要定义可能永远不会完成的递归-将其称为mu-递归运算符

要定义这样一个运算符,需要一个无限的已定义标识符空间(如果对每个标识符进行有限次数的评估),因为无法知道先验找到结果的时间上限。在代码内部包含有限数量的运算符的情况下,人们需要能够检查无限数量的可能性。

因此,此类函数无法由C预处理程序计算因为在C预处理程序中,定义的宏数量有限,并且每个宏仅扩展一次。

C预处理程序使用Dave Prosser的算法(由Dave Prosser在1984年为WG14团队编写)。在这种算法中,宏在第一次扩展时就被涂成蓝色。递归调用(或相互递归调用)不会扩展它,因为在第一次扩展开始时它已经被涂成蓝色。因此,使用有限数量的预处理行,就不可能对函数(宏)进行无限调用,这是mu递归运算符的特征。

C预处理器只能计算sigma递归运算符

有关详细信息,请参阅Marvin L. Minsky(1967)的计算过程-计算:有限和无限机器,Prentice-Hall公司,Englewood Cliffs,NJ等。


Ackerman函数仅是mu递归的,可以在PP中实现,因此C预处理器不仅限于sigma递归运算符:gist.github.com/pfultz2/80391e8b18abf3225da2242dcc570cec
Paul Fultz II,

2
就像我在另一条评论中所说的,这是您所做的,检查大量(但数量有限)输入和搜索无限数之间的区别。已知Ackerman函数可以完成,因此cpp可以找到其值。无法计算任何mu运算符并完成cpp调整。
alinsoar

@PaulFultzII您需要修改代码以检查更大的解决方案空间,而mu运算符是固定的,并且将检查无限的空间(在硬件资源允许的范围内)。
alinsoar

A不需要修改算法(即宏),只需更新评估以添加更多扫描。
Paul Fultz II

4

Turing在限制内完成(所有计算机也一样,因为它们没有无限的RAM)。检查一下您可以使用Boost Preprocessor进行的操作

根据问题编辑进行编辑:

Boost的主要限制是特定于编译器的最大宏扩展深度。同样,实现递归的宏(FOR ...,ENUM ...等)并不是真正的递归,由于有一系列几乎相同的宏,它们才以这种方式出现。从总体上看,此限制与实际递归语言中的最大堆栈大小没有什么不同。

对于有限的图灵完备性(Turing-compatibility?)而言,唯一真正需要的两件事是迭代/递归(等效构造)和条件分支。


你好 这实际上是提示我的问题的原因,我使用预处理器已有一段时间了。
Anycorn

深入研究BOOST_PP的源代码是弄清楚如何完成的最佳方法。
齿轮

13
相信宏不能进行递归。升压似乎只是通过其命名为喜欢的硬编码宏来模拟他们macro0macro1.. macro255。我不确定这是否算作“完成”。预处理器有一个明确的规则,禁止从macro255回溯到macro0:(似乎试图使用有限状态自动机为完全括号化的表达式构建验证器。它可以在有限的括号中使用,但是现在不再是通用的验证器。我没有关于boost.pp内部运作的线索了,所以我可以很可能是错误的这一点。
约翰内斯·绍布- litb

@Johannes Schaub:是的,您是对的。最初写这篇文章时,我已经将其与vararg混淆了。我更新了答案。
齿轮

5
@Johannes:混乱的预处理器没有这样的宏。在此处查看:sourceforge.net/projects/chaos-pp
Joe D
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.