研究 Boost.Preprocessor 的时候整理的一套”宏元编程”套路:用纯宏实现在编译期展开出 N 个变量。不涉及任何编译器扩展,纯标准 C 预处理器。

目标

1
2
int BOOST_PP_ENUM_PARAMS(20, a);
// 展开后:int a0, a1, a2, ... , a19;

坑一:拼接参数必须经过两层宏

1
2
3
#define BOOST_PP_CAT(a, b) BOOST_PP_CAT_I(a, b)
#define BOOST_PP_CAT_I(a, b) BOOST_PP_CAT_II(~, a##b)
#define BOOST_PP_CAT_II(p, res) res

为什么不能直接写 a##b?因为 ##阻止参数展开——直接 CAT(BOOST_PP_REPEAT_, 1) 拼出来的是字面量 BOOST_PP_REPEAT_1 而不是它展开后的内容。标准解法就是延迟一层:先拼出 ~, BOOST_PP_REPEAT_1,再扫描一轮时 ~ 被丢弃、参数正常展开。

坑二:循环靠”手写递归表”

预处理器没有循环,只能靠宏递归展开模拟。但宏不能直接递归引用自己,所以每个编号的规则引用比它小一号的规则:

1
2
3
4
#define BOOST_PP_REPEAT_1_1(m, d)  m(2, 0, d)
#define BOOST_PP_REPEAT_1_2(m, d) BOOST_PP_REPEAT_1_1(m, d) m(2, 1, d)
#define BOOST_PP_REPEAT_1_3(m, d) BOOST_PP_REPEAT_1_2(m, d) m(2, 2, d)
// ...一直写到 25

REPEAT_1_3 展开成 REPEAT_1_2 加上 m(2,2,d),层层展开就得到了”调用 m 0 次、1 次、2 次”的序列。想支持多少次展开,就把表写多长。

坑三:if/else 靠”查找表”

1
2
3
4
5
6
7
8
#define BOOST_PP_BOOL_0 0
#define BOOST_PP_BOOL_1 1
#define BOOST_PP_BOOL_2 1
// ...所有数字枚举一遍

#define BOOST_PP_IIF_0(t, f) f
#define BOOST_PP_IIF_1(t, f) t
#define BOOST_PP_COMMA_IF(cond) BOOST_PP_IF(cond, BOOST_PP_COMMA, BOOST_PP_EMPTY)()

条件为 0 还是 1,靠 BOOST_PP_BOOL_数字 这张查找表把任意数字归一化成 0/1,再由 IIF_0/IIF_1 选择分支。COMMA_IF 则用来决定两个变量之间要不要补逗号(最后一个不加)。

展开过程

ENUM_PARAMS(20, a) 最终展开为:

1
a0, a1, a2, a3, ..., a18, a19

这套东西就是 Boost.Preprocessor 的核心原理,C++ 模板元编程出现之前,库作者全靠它生成重复代码。调试方法:gcc -E 看预处理结果,一切尽收眼底。