GitHub上一个2015年的C语言仓库,最近被翻出972颗星。不是新框架,不是工具链,是一堆让人瞳孔地震的宏定义——用预处理指令实现条件判断、循环、甚至递归。
Paul Fultz II写的这份文档,标题朴素得像教科书附录:《C Preprocessor tricks, tips, and idioms》。但点进去的人,多半会经历三个阶段:这什么鬼→有点意思→我CPU烧了。
「##」运算符的暗面:连接符成了拦路虎
宏拼接(token pasting)的核心是`##`运算符。你想写个`IIF`宏实现布尔选择?直觉写法长这样:
#define IIF(cond) IIF_ ## cond
cond为0时展开成`IIF_0`,为1时展开成`IIF_1`,再分别定义这两个宏返回第二个或第一个参数。逻辑通顺,但有个致命陷阱。
问题出在`##`的副作用:它会抑制宏展开。假设你定义了`#define A() 1`,然后调用`IIF(A())(true, false)`。预期结果是`true`,实际展开却是`IIF_A()(true, false)`——`A()`根本没被求值,直接被当成字符串粘过去了。
Fultz的解法是多套一层壳。定义`CAT`宏做间接拼接:
#define CAT(a, ...) PRIMITIVE_CAT(a, __VA_ARGS__)
#define PRIMITIVE_CAT(a, ...) a ## __VA_ARGS__
调用`CAT(IIF_, 1)`时,先展开参数变成`PRIMITIVE_CAT(IIF_, 1)`,这时候`##`才出手,得到`IIF_1`。多这一步,就绕过了抑制展开的坑。
这套`IIF`最终形态是:
#define IIF(c) PRIMITIVE_CAT(IIF_, c)
#define IIF_0(t, ...) __VA_ARGS__
#define IIF_1(t, ...) t
现在`IIF(A())(true, false)`能正确走到`true`了。这个模式被复用到一堆布尔运算宏里:`COMPL`取反、`BITAND`与运算,全是拼接+模式匹配的套路。
计数器的暴力美学:从0数到9,手写10个宏
C预处理器没有变量,但你可以假装有。`INC`和`DEC`宏用纯拼接实现加减——限制在0到9范围,因为再往上宏展开深度会炸。
实现方式毫无取巧:穷举。
#define INC(x) PRIMITIVE_CAT(INC_, x)
#define INC_0 1
#define INC_1 2
...一路写到`INC_9 9`(9加1溢出截断)。`DEC`同理,从`DEC_1 0`写到`DEC_9 8`。
看起来蠢,但这是图灵完备的地基。有了条件判断和加减,你就能控制循环次数。后面要讲的`REPEAT`宏,核心就是靠`INC`/`DEC`当计数器用。
探测技术:用变参数量子力学测"有没有"
检测一个值是否存在,或者检测参数是不是括号,这是预处理的黑魔法领域。Fultz用的技巧 exploit 了变参宏的一个特性:逗号分隔的参数数量会影响展开结果。
核心是两个宏:
#define CHECK_N(x, n, ...) n
#define CHECK(...) CHECK_N(__VA_ARGS__, 0,)
#define PROBE(x) x, 1
调用`CHECK(PROBE(~))`时,`PROBE(~)`先展开成`~, 1`,所以整体变成`CHECK(~, 1)`。再进`CHECK_N(~, 1, 0,)`,取第二个参数,返回1。
但如果`PROBE`没展开(比如探测的目标不存在),`CHECK`直接拿到的是单个值,展开后变成`CHECK_N(那个值, 0,)`,返回0。
这个1和0就是布尔信号。文档里用这套实现了`IS_PAREN(x)`检测括号、`IS_EMPTY(x)`检测空参数,甚至能区分`0`和`()`——在C宏的世界里,这相当于类型系统了。
递归的幻觉:宏怎么调用自己
预处理器明令禁止直接递归:`#define A() A()`会导致无限展开,编译器会掐断。但间接递归可以——只要你每次换个名字。
Fultz的`REPEAT`宏用了一个"扫描-再扫描"技巧。定义两个互相指涉的宏,`REPEAT`和`REPEAT_`:
第一次扫描时,`REPEAT`展开成包含`REPEAT_`的形式;第二轮扫描时,`REPEAT_`再展开回`REPEAT`。中间插一个`EMPTY()`延迟宏,控制什么时候触发下一轮。
实际代码长这样(简化版):
#define REPEAT(count, macro, ...) \
WHEN(count) \
OBSTRUCT \
REPEAT_INDIRECT \
(DEC(count), macro, __VA_ARGS__) \
macro(DEC(count), __VA_ARGS__) \
`OBSTRUCT`和`WHEN`是条件控制宏,`REPEAT_INDIRECT`就是`REPEAT`的别名。每次递归计数器减1,到0时`WHEN`阻断展开。
这套机制能展开256层——受限于C标准规定的宏展开深度上限。对代码生成来说够用了:批量定义结构体字段、展开模板代码、甚至写有限的元编程。
从技巧到工程:Boost.PP和实际落地
Fultz写这份文档时,Boost.Preprocessor库已经存在多年。那个库用类似技巧实现了更完整的预处理元编程,但依赖关系重、编译慢、错误信息像天书。
这份文档的价值在于"最小可运行演示"。972个星标里,估计一半是收藏即学会,另一半是真拿去做代码生成了。嵌入式领域有人用它生成寄存器映射表,游戏引擎用它做反射系统的胶水层。
有个细节值得玩味:文档最后修订于2015年6月15日,C++14刚发布不久。那时候constexpr还没现在这么能打,模板元编程又编译慢,预处理宏是折中方案。现在C++20有了consteval,编译期计算能力暴涨,这类技巧的实用价值在下降。
但GitHub的星星数还在缓慢上涨。或许因为C语言本身没这些新特性,或许因为有人发现预处理宏的展开速度比模板快一个数量级,又或许纯粹是——看着`#define`实现图灵完备,有种看汇编写操作系统的颅内高潮。
文档的14次修订记录里,最早版本可以追溯到2013年。Fultz在issue区回复过一条评论:「这些技巧不是为了生产代码,是为了证明C预处理器不是纯文本替换器。」
你现在会把这套技巧用在什么场景?是宁愿忍受宏的调试地狱换编译速度,还是直接上现代C++的模板元编程?
热门跟贴