最近在 Reddit 的 r/C_Programming 和 r/compilers 两个社区发帖讨论了一个问题:
当某个编程模式被大量项目反复实现时,它是否应该从库升格为语言特性?
这个看似简单的问题,引发了两场很有价值的讨论,也让我重新思考了语言设计的边界。
C 语言其实一直在“克制地演进”
很多人认为 C 几十年来几乎没有变化。
这种说法并不准确。
C 一直在发展,只是它的发展方式非常谨慎。它并不会因为某个功能“方便”就加入语言,而是会考虑:
- 这个特性是否需要编译器理解?
- 是否影响类型系统、优化或者内存模型?
- 是否能在不同平台上保持统一语义?
C 的设计哲学并不是追求更多功能,而是在寻找一个边界:
哪些信息应该继续隐藏在库后面,哪些信息已经必须成为编译器理解的语义。
例如:
memcpy()
printf()
malloc()
这些功能非常重要,但它们本质上属于代码复用问题。
开发者需要调用它们,但编译器不需要理解它们背后的抽象模型,因此留在库中更加合理。
而 C11 的 _Atomic 则是一个经典的反例。原子操作早期可以用库函数实现,但编译器不知道“这个变量是原子的”,就无法正确处理内存序、指令选择和优化限制。只有把 _Atomic 纳入语言,编译器才能生成正确的代码。
GObject:库能解决,但代价不小
讨论中被多次提到的 GObject 是很好的案例。它在纯 C 上构建了一套完整的对象系统(类型、继承、信号、反射)。这说明 C 程序员确实有更强抽象的需求。
但 GObject 依然停留在库层面,因为它对编译器来说仍然只是“结构体 + 函数指针”。编译器看不到类关系、方法分派等信息,导致优化受限、代码碎片化。
这正是核心矛盾:库足够灵活,但重复实现带来了维护成本和兼容性问题。
两个社区的视角差异
- r/C_Programming:更关注稳定性。“库能解决就用库,为什么改语言?”增加语言特性意味着编译器支持、标准维护、长期兼容等沉重负担。
- r/compilers:更关注表达力和优化。“如果编译器能理解这个抽象,能否带来本质上更好的代码生成?”
这种差异很有代表性:一个守护 C 的小而美,另一个追求语言能力的边界。
静态组合 vs 语言原语
有人提出,很多抽象可以通过模板、trait、comptime 等机制在编译期静态组合(C++、Rust、Zig 都在这么做)。这确实是强大且优雅的方向。
但组合已有机制和让编译器原生理解新抽象不是一回事。泛型就是一个例子:宏、模板、运行时都能模拟,但只有语言级支持才能让类型关系成为编译器可优化的第一等公民。
异构计算正在挑战边界
今天,代码可能运行在 CPU、GPU、NPU 等不同设备上。CUDA/OpenCL 等方案用 API 解决了问题,但开发者仍需面对不同的内存模型和编译流程。
“代码在哪里执行”——这到底只是一个库调用,还是正在成为需要语言表达的新语义?
真正的问题不是“C 要不要变高级”
最容易引起误解的表述是:“C 该不该成为高级语言?” 这会让很多人立刻进入防御模式,以为有人想把 C 变成 C++。
更准确的问题是:
哪些抽象应该留在库,哪些应该成为语言和编译器能理解的能力?
C 的成功正源于它清晰的边界。未来 C 是否需要调整边界,不取决于“功能越来越多”,而取决于软件世界是否出现了新的基础概念,这些概念已经无法被简单库机制优雅地表达。
我的思考(AET 的视角)
这次讨论让我意识到,语言设计中的争论往往不是“支持增加功能”和“反对增加功能”的简单对立,而是两个问题的权衡:抽象能力的收益,和语言复杂度的长期成本。
在设计 AET 的过程中,我也在持续思考这个问题。AET 不是简单给 C 堆语法,而是尝试让编译器理解更多程序员真正关心的信息:泛型语义、执行设备、扩展的目标模型等。
语言的真正价值,不只是写得更方便,而是让编译器和程序员站在同一边。
总结本文的中心思想,其实就是下面这个流程图
库模式
|(边界?)
语言抽象
|
编译器语法




