C语言宏不支持真递归,因其预处理器仅做纯文本替换、无算术运算与条件终止能力;所谓“递归展开”实为通过多层间接宏(如LOOP_N→LOOP_N_IMPL)、命名后缀(LOOP_8/LOOP_7)及终止哨兵(LOOP_0)模拟,依赖##拼接与两层宏绕过参数冻结限制,用于生成预处理期可见的固定结构代码。

宏的多轮递归展开不是“真递归”,而是靠预处理器多层间接调用+计数控制实现的;它比模板元编程更底层、更不可调试,但能在 #include 前就完成文本生成,适合生成固定结构的样板代码(如注册表、序列化字段、枚举字符串映射)。
为什么不能直接写 #define RECURSE(x) RECURSE(x-1)
预处理器不支持算术运算或条件终止,x-1 不会被计算成数字,而是原样保留为文本,导致无限展开或报错 error: macro "RECURSE" requires 1 argument, but only 0 given。必须把“次数”硬编码为宏名的一部分,靠命名规则触发不同层级的定义。
- 预处理器只做纯文本替换,不理解数值、类型或作用域
- 所有“计数”逻辑都靠宏名后缀(如
LOOP_8、LOOP_7)和特化定义驱动 - 必须用至少两层宏(例如
LOOP_N→LOOP_N_IMPL)绕过参数禁止重展开限制
怎么写出能展开 N 层的宏(以生成 field_0 到 field_7 为例)
核心是“间接层 + 后缀匹配 + 终止哨兵”。下面是最小可行写法:
#define LOOP_0(...) #define LOOP_1(x) x##0 #define LOOP_2(x) LOOP_1(x) x##1 #define LOOP_3(x) LOOP_2(x) x##2 #define LOOP_4(x) LOOP_3(x) x##3 #define LOOP_5(x) LOOP_4(x) x##4 #define LOOP_6(x) LOOP_5(x) x##5 #define LOOP_7(x) LOOP_6(x) x##6 #define LOOP_8(x) LOOP_7(x) x##7 #define LOOP_N(n, x) LOOP_##n(x)
调用 LOOP_N(5, field_) 展开为 field_0 field_1 field_2 field_3 field_4。关键点:
-
LOOP_N是入口,靠##拼出具体层数宏名 - 每一级
LOOP_k都显式调用前一级,并追加自己的后缀项 -
LOOP_0是终止符,展开为空,防止继续递归 - 不能依赖
__COUNTER__或__LINE__做动态计数——它们无法回溯或条件跳转
容易踩的坑:参数被提前字符串化或粘连失败
常见错误是想用 # 或 ## 时参数没展开就冻结了。比如:
#define STR(x) #x #define CAT(a, b) a##b STR(CAT(foo, bar)) // 展开为 "CAT(foo, bar)",不是 "foobar"
正确写法必须加间接层:
#define STR_(x) #x #define STR(x) STR_(x) #define CAT_(a, b) a##b #define CAT(a, b) CAT_(a, b) STR(CAT(foo, bar)) // 展开为 "foobar"
- 只要宏体里出现
#或##,其直接参数就不会再展开(预处理第二阶段冻结) - 必须用中间宏(如
STR_、CAT_)把参数先完全展开,再送进带#/##的宏 - 宏展开深度受编译器限制(GCC 默认 600 层),超过会报
fatal error: macro recursion limit reached
什么时候该用宏展开,而不是 std::index_sequence 或 if constexpr
宏展开唯一不可替代的场景,是生成**预处理器可见的符号**:头文件卫士、#pragma once 替代、#ifdef 分支、enum 值与字符串映射表、offsetof 批量计算。其他所有运行时逻辑、类型推导、SFINAE 友好操作,一律用 C++17 起的模板方案。
- 宏生成的是纯文本,不能产生新类型、新函数签名或依赖模板参数推导的逻辑
-
std::make_index_sequence能让编译器在模板中安全展开,而宏展开失败时只有预处理错误行号,无类型信息 - 跨平台项目中,MSVC 和 GCC 对宏嵌套深度、
__VA_ARGS__处理有细微差异,模板方案更稳定
真正难的不是写多层宏,而是确定哪部分必须在预处理期完成——一旦误判,后面全是 debug 地狱。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











