C语言递归宏详解
一、先把地基压实:C 宏并不会“真递归”
C 标准(以 C17 为例)规定了宏展开的单次扫描与不立即再次展开相同标识符的规则。简化地说,这段代码不会像函数那样在运行期一层层压栈,而是在编译前由预处理器做一次(或多次)符号替换。
#define SELF(X) SELF(X) // 期待“递归”?不会成功
int x = SELF(1); // 预处理器直接拒绝/死循环保护
要点:所谓“递归宏”,实际上是通过构造“延迟展开(defer)”和“多轮扫描(rescan)”,让预处理器在后续轮次再去展开目标,从而模拟“递归”的效果。
二、故事线索:一份命令表,三处复用
项目里有一张“命令定义清单”。经典解法是 X-Macro:把数据集中放在一个表里,用不同的宏姿势多次“播放”。
// commands.def —— 只维护这一份
// 格式: X(标识符, "字符串", 编码)
X(CMD_FOO, "foo", 0x01)
X(CMD_BAR, "bar", 0x02)
X(CMD_BAZ, "baz", 0x03)
然后在不同位置这样用:
// 1) 枚举
#define X(name, str, code) name = code,
enum Command {
#include "commands.def"
};
#undef X// 2) 字符串数组
#define X(name, str, code) [code] = str,
const char* kCmdName[0x100] = {
#include "commands.def"
};
#undef X
这已经解决了 “一处维护、多处生成” 的大半问题。可一旦想“遍历变长参数”或“编出嵌套结构”,就会撞到 X-Macro 的边界,于是“递归宏”的戏份来了。
三、递归宏的核心技巧:延迟与阻断
想象成一句“别现在展开,等下一轮再说”。常见做法是准备三个工具宏:
#define EMPTY()
#define DEFER(id) id EMPTY() // 把 id 的展开推迟一轮
#define OBSTRUCT(...) __VA_ARGS__ DEFER(EMPTY)() // 阻断+再延期
#define EXPAND(...) __VA_ARGS__ // 有时配合编译器做再展开
它们本身不神秘,重点在让预处理器别一次性把所有层级吃完。这样才能模拟“你先记着,下一口再嚼”的节奏。
例子1:给可变参数“逐个套壳”(MAP)
目标:把 MAP(WRAP, A, B, C) 变成 WRAP(A) WRAP(B) WRAP(C)。一份最小可用(且便于理解)的实现如下:
// 判空与“还有参数吗”的探针(简化版)
#define EVAL(...) EVAL1(EVAL1(EVAL1(__VA_ARGS__)))
#define EVAL1(...) EVAL2(EVAL2(EVAL2(__VA_ARGS__)))
#define EVAL2(...) __VA_ARGS__#define CAT(a,b) a##b
#define PROBE() ~, 1
#define IS_PROBE(...) CAT(IS_PROBE_, __VA_ARGS__)
#define IS_PROBE_~ , 0#define SECOND(a, b, ...) b
#define NOT(x) IS_PROBE(CAT(PROBE_ , x))
#define BOOL(x) NOT(NOT(x))
#define IF(c) CAT(IF_, c)
#define IF_1(t, ...) t
#define IF_0(t, ...) __VA_ARGS__// 取第一个参数与余下参数
#define HEAD(x, ...) x
#define TAIL(x, ...) __VA_ARGS__// 检测是否还有参数(极简近似)
#define HAS_ARGS(...) BOOL(SECOND(__VA_ARGS__ , ~))// 递归样式的 MAP
#define MAP(f, first, ...) \f(first) \IF(HAS_ARGS(__VA_ARGS__))( \OBSTRUCT()(DEFER(MAP)()(f, __VA_ARGS__)) \, /* empty */)#define WRAP(x) [x]/* 使用 */
EVAL(MAP(WRAP, A, B, C)) // 结果: [A] [B] [C]
这里的关键就是 OBSTRUCT 和 DEFER:当 MAP 需要“再处理余下参数”时,它不立刻调用自己,而是故意推迟一个展开节拍,交给下一轮扫描,于是在视觉上形成了“递归”。
例子2:把键值对转成结构初始化
当命令表变复杂,可能出现嵌套:
// pairs.def
P(kFoo, 1)
P(kBar, 2)
P(kBaz, 3)
想要生成:
struct Item { const char* name; int code; };
struct Item items[] = {{"kFoo", 1}, {"kBar", 2}, {"kBaz", 3}
};
可以在 P 前后套“递归 MAP”:
#define AS_ITEM(pair_name, code) { #pair_name, code },#define LIST(...) EVAL(MAP(AS_ITEM, __VA_ARGS__))struct Item items[] = {LIST(// 让 def 文件只管内容,外面用“展开器”来遍历#define P(name, code) name, code#include "pairs.def"#undef P)
};
四、工程可落地的三板斧
1)把“算法”封到独立头文件:例如 pp_map.h 专管 MAP/CHAIN/REPEAT 等。业务侧只 include,不反复复制宏魔法。
2)给每条“魔法”写一次“快照单测”:对编译后产物用 static_assert 或数组大小校验,确保未来改动不把生成结果弄坏。
3)设置最大深度:递归宏最终还是有限步“吃完”。为 MAP 等提供“最大次数保险丝”,避免极端输入卡在预处理阶段。
// 例如硬性限制最多处理 64 项
#define LIMIT_64(...) __VA_ARGS__
#define MAP_LIMITED(f, ...) LIMIT_64(EVAL(MAP(f, __VA_ARGS__)))
五、与现成库牵手:Boost.Preprocessor 的启发
C++ 项目常用的 Boost.Preprocessor 在 C 代码里也能借鉴思想(宏只是预处理期产物)。它提供 BOOST_PP_REPEAT、BOOST_PP_SEQ_FOR_EACH 等成熟原语,足以覆盖多数“列表扫描”“计数”“拼接”需求。
Boost.Preprocessor 官网:
https://www.boost.org/doc/libs/release/libs/preprocessor/
六、再回到故事:三处生成,一处真相
命令清单只维护在一处;
通过 X-Macro 完成“结构性多次播放”;
在需要“变长/嵌套遍历”的地方,用**递归宏(延迟展开)**把“列表加工”补齐。
落地后,增删命令就只改一行,枚举、字符串、解析器保持同步。
七、边界与避坑清单(工程经验浓缩)
- 可读性:宏层层交错非常晦涩。务必写注释,尤其解释
DEFER/OBSTRUCT的意图与“多轮扫描”的必要性。 - 编译器差异:不同编译器对“再次扫描”的策略略有差别;遇到怪异行为,优先用
EVAL/EVAL1/EVAL2...逼出足够的展开轮次。 - 调试手段:
-E导出预处理结果;或在 IDE 里开启“预处理产物”查看。 - 边做边收敛:一旦逻辑成长为“编程语言中的编程语言”,要审视是否引入 Python/生成脚本更合适。
九、再给两个“像真递归”的小玩具
计数:编译期统计参数个数
#define ARG_N( \_1,_2,_3,_4,_5,_6,_7,_8,_9,_10, \_11,_12,_13,_14,_15,_16,_17,_18,_19,_20, \N, ...) N
#define RSEQ() \20,19,18,17,16,15,14,13,12,11,10, \9, 8, 7, 6, 5, 4, 3, 2, 1, 0#define NARGS_(...) ARG_N(__VA_ARGS__)
#define NARGS(...) NARGS_(__VA_ARGS__, RSEQ())// 用法
static int a[NARGS(A,B,C) == 3 ? 1 : -1]; // 通过即为 3
在宏里“折叠”一棵小表达式树
#define PLUS(a,b) ((a)+(b))
#define TIMES(a,b) ((a)*(b))// FOLD(PLUS, 0, 1,2,3) => (((0+1)+2)+3)
#define FOLD(op, init, first, ...) \op(init, first) \IF(HAS_ARGS(__VA_ARGS__))( \OBSTRUCT()(DEFER(FOLD)()(op, /* init= */ op(init, first), __VA_ARGS__)) \, /* empty */)
十、参考资料(中途穿插,便于继续深挖)
GCC 预处理器手册:
https://gcc.gnu.org/onlinedocs/cpp/
Clang -E 预处理输出说明:
https://clang.llvm.org/docs/CommandGuide/clang.html#preprocessing-options
Boost.Preprocessor(宏元编程原语):
https://www.boost.org/doc/libs/release/libs/preprocessor/
