OCaml 居然能“编译成 C++”?这场看似离谱的实验,戳中了编译器世界最有趣的一根神经

开发工具 2026年4月2日
OCaml 居然能“编译成 C++”?这场看似离谱的实验,戳中了编译器世界最有趣的一根神经
OCaml 社区最近出现了一则颇具黑色幽默意味的 Pull Request:给 ocamlc 加上一个“C++ 后端”,把 OCaml 程序翻译成模板元编程风格的 C++。它表面上像玩笑,实际上却精准揭示了编程语言、编译器和“图灵完备宿主语言”之间那条暧昧又迷人的边界。

一场程序员笑到一半会沉默的“发布”

如果你最近逛了 OCaml 的 GitHub 仓库,可能会看到一个让人忍不住多看两眼的 Pull Request:有人给 ocamlc 加了一个新的 C++ 后端。命令也写得一本正经,ocamlc -incr-c primes.ml,看起来仿佛下一秒就能把 OCaml 项目无缝融入现代 C++ 工业体系。

但你往下看,很快会意识到,这不是我们日常理解的“把语言编译到 C++ 以获得可移植性”那种路线,而是一场非常典型、非常高级、也非常程序员的恶作剧——或者说,技术讽刺。生成出来的 primes.cpp 确实是 C++,而且还是“可读的、惯用的 C++”,只是这个“惯用”要打上巨大引号:它主要依赖模板、类型系统和编译期求值,把一个求素数的 OCaml 程序,变成了让 C++ 编译器在报错信息里“打印结果”的元编程怪物。

最妙的地方在于,这个 PR 的叙述口吻极其严肃。作者一本正经地说,C++ 是一种“纯函数式语言”,不支持可变状态,所以 OCaml 标准库里那些用到 mutation 的部分暂时不能直接用;接着又把 g++ 称作“C++ 解释器”,说它可以通过 -Dlimit=100main 传参数。程序跑完之后,输出不是正常 stdout,而是编译错误信息里那串层层嵌套的 Cons<I<2>, Cons<I<3> ...>>。你一边读一边笑,但笑着笑着也会发现:这玩笑之所以成立,是因为它建立在真实而扎实的编译器技术之上。

这不是单纯整活,它在提醒我们:语言的边界没那么硬

很多人第一次接触这种项目,会把它归类为“黑魔法展示”。这没错,但只说到这一步,就低估了它的价值。这个 C++ 后端最有意思的地方,不在于它能不能实用,而在于它再次证明了一件事:只要目标系统足够强大,所谓“编译到某种语言”这件事,远比我们想象得更宽泛。

C++ 模板系统这些年一直被戏称为“意外长成的函数式语言”。模板特化、类型递归、常量表达式,本来只是为了泛型编程和编译期优化准备的工具,结果一路演化成了一个近乎另类计算平台。老程序员对这一幕并不陌生:早年 Boost.MPL、Template Haskell、Lisp 宏系统、甚至后来 Rust 的类型体操,本质上都在干一件类似的事——把“编译过程”本身变成一台可编程机器。

这次 OCaml 的 PR,等于把这层关系撕开给大家看:你以为 C++ 只是目标语言,但在某些极端设定下,它也可以成为运行时;你以为编译器只是翻译器,但在另一些场景里,编译器本身就是解释器、求值器,甚至是输出设备。程序结果藏在报错信息里,这当然荒诞,可荒诞背后是一个严肃事实:现代语言工具链之间的边界,越来越像工程约定,而不是物理定律。

这也是为什么很多编译器工程师会对这种项目会心一笑。它不是在宣布“OCaml 终于支持 C++ 生态”,而是在借一个足够夸张的形式,重新追问一个老问题:我们究竟是在“运行程序”,还是在“诱导另一个系统替我们完成计算”?从宏展开到 SQL 查询优化,从 Tensor 编译到 GPU kernel 生成,这种问题今天一点都不过时。

从性能笑话,到编译器现实

当然,玩笑之所以好笑,也因为它精准踩中了 C++ 的几个经典梗。比如更大程序默认不支持,得加 -ftemplate-depth=999999;比如算到 10000 以内素数,g++ 半分钟、11 GiB 内存;再比如 clang++ 更“高效”,一秒多就能给你打印 warning 然后 segfault。这样的描述几乎是把编译器开发者和 C++ 用户共同的 PTSD 写成了段子。

但别急着把它只当段子看。它其实也揭示了一个真实问题:图灵完备不等于适合计算,能表达不等于能高效执行。 这在今天尤其重要,因为 AI 热潮之下,越来越多公司把各种系统吹成“通用推理引擎”“统一执行后端”“全栈编译目标”。从 TVM、XLA 到 MLIR,大家都在讲抽象层、可移植性、统一中间表示,这些方向都很有价值,但这个 PR 用最夸张的方式提醒大家:如果你只盯着“理论上能做到”,很容易忽略实际成本会有多离谱。

作者也没有停留在搞笑层面,而是进一步讨论了算法问题。最初那个纯函数式筛法并不高效,后来换用更好的优先队列算法和左偏堆数据结构后,g++ 计算 10000 以内素数的时间缩短到 8 秒,内存降到 3.1 GiB。你会觉得这数字依然荒唐,但这恰恰是编译器圈特别迷人的地方:哪怕在一个本来就不该做这件事的系统里,算法和数据结构依旧能显著改变结果。工程世界从来不是“能”与“不能”这么简单,中间永远隔着一层实现细节的深海。

为什么这件事发生在今天,反而格外有意思

如果放在十年前,这样的 PR 大概只会在黑客社区里小范围传播;但放在今天,它更像是对整个软件行业的一次轻轻敲打。过去几年,开发者越来越习惯跨语言、跨运行时、跨平台地思考问题:Rust 写内核,TypeScript 跑服务端,Python 调度 GPU,WebAssembly 试图成为“通用便携字节码”,连数据库都在变成程序执行环境。大家对“程序最终跑在哪里”这件事,已经没有过去那么教条。

也正因为如此,这种“把 OCaml 编译成 C++ 模板程序”的荒诞实验,反而显得特别应景。它让人想起一个常被忽略的事实:编程语言之间并不是泾渭分明的阵营,更多时候,它们是层层嵌套、互相借力的技术结构。你可以把高级语言降到 C,再交给 LLVM;你可以把 DSL 编到 SQL;你甚至可以把业务逻辑塞进类型系统、宏系统、编译错误、构建脚本。今天的软件栈早就不是线性的,而是立体的。

这个 PR 还顺手点到了 Rust。作者提到,等 Rust 支持更完整的 partial impl specialization,它理论上也可能成为运行 OCaml 程序的宿主。这听着仍然像玩笑,但方向感并不假。Rust 的 trait 系统、const eval、类型层计算能力,这几年已经让很多开发者意识到:未来语言竞赛,未必只拼运行时性能,也会拼“编译器本身到底能承载多少计算”。

这对行业是个挺有意思的信号。语言设计正在越来越多地被编译期能力牵引,库作者也越来越愿意把复杂性前移到编译阶段。收益是更强的静态保证、更少的运行时开销;代价则是编译变慢、错误信息爆炸、心智负担上升。OCaml 这个 PR 用戏谑方式,把这笔账摊在了桌面上。

真正值得讨论的,不是它能不能用,而是我们会不会越走越远

我并不认为这个 C++ 后端会真的进入 OCaml 的正式路线图。它几乎肯定不会成为生产特性,也没有人会认真建议你拿它部署服务或构建桌面应用。但它提出了一个在今天非常现实的问题:我们是不是越来越习惯把本不该承担的复杂性,推给编译器和工具链?

从 C++ 模板地狱,到 Rust 类型体操,再到前端生态里层层嵌套的构建链条,过去十年软件工程一个明显趋势,就是把越来越多工作前置到“生成”“检查”“推导”“自动化”环节。好处显而易见:更安全、更可移植、更自动。但坏处也在累积:构建时间失控、错误难懂、工具链脆弱,最终让开发者在“程序真正开始运行之前”就已经筋疲力尽。

所以,OCaml 这个 PR 最高明的地方,恰恰是它没有正面说教,而是让你自己感受到那种荒谬。一个本来应该在终端里打印 [2;3;5;7;...] 的程序,最后要靠编译器报错信息来“显示输出”;一个计算素数的小例子,能把内存吃到 11 GiB。它像是在问整个行业:我们当然可以继续把语言做得更强、类型系统做得更深、编译器做得更聪明,但什么时候,这份聪明会开始反过来消耗人?

也许这就是这类作品最可贵的地方。它不是产品,不是 roadmap,不是融资故事,也不是一篇空洞的“XX 语言将颠覆未来”。它是一面哈哈镜,照出来的却是真问题。技术圈有时候太需要这种作品了——让我们笑一笑,然后重新想想,自己到底在造什么。

Summary: 这次 OCaml“支持 C++ 后端”的消息,表面是黑客式玩笑,内里却是一次非常精准的技术评论。它提醒我们,语言和编译器的能力边界远比想象中松动,但也提醒我们:能把事情塞进编译期,不代表就应该这么做。我的判断是,这类项目不会改变主流开发路线,却会持续影响语言设计者和编译器工程师的思考方式。未来几年,围绕“编译期计算”与“工具链复杂性”的拉扯,只会越来越明显。
OCaml编译器C++ocamlcC++ 后端模板元编程Pull Request图灵完备函数式编程GitHub