当前位置: 首页 > news >正文

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]

这里的关键就是 OBSTRUCTDEFER:当 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_REPEATBOOST_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/

http://www.dtcms.com/a/577076.html

相关文章:

  • 指令微调(Instruction Tuning)
  • Linux 中 NIC(网络接口卡)和协议栈的区别以及DPDK
  • MATLAB实现贝叶斯回归预测
  • ZYNQ介绍
  • 【Python】-- 趣味代码 - Piano游戏
  • 解决使用EasyExcel导出带公式的excel,公式不自动计算问题
  • 展示型网站多少钱建设大型网站的公司
  • 前端FAQ: 描述⼀下你最近使⽤过的前端框架,并解释为何选择它们?
  • [特殊字符] 微前端部署实战:Nginx 配置 HTTPS 与 CORS 跨域解决方案(示例版)
  • 短视频矩阵系统搭建指南:源码部署与全流程解析
  • 李沐动手学深度学习笔记(1)
  • 做网站云服务器选择多大带宽北京网站建设有哪些公司好
  • 第8章 模块系统
  • GraphRAG在Windows环境下离线部署
  • Spring Boot 实战:企业级接口限流与熔断机制设计
  • 二十一、二进制文件部署高可用集群
  • 窗口dp|组合数学
  • 【linux国庆练习】
  • 织梦cms怎么做双语网站wordpress网页小特效
  • 我的世界做壁纸的网站移动互联网开发心得体会
  • CST对电路板与地面平面耦合的电磁模拟
  • Apple授权登录开发流程
  • 告别手动导出:一键将思源笔记自动同步到 Git 仓库
  • OPPO 后端校招面试,过于简单了!
  • element表格的行列动态合并
  • C++ 零基础入门与冒泡排序深度实现
  • 鸿蒙harmony将注册的数据包装成json发送到后端的细节及过程
  • JavaWeb(后端进阶)
  • VOC浓度快速测定仪在厂界预警中的实战应用:PID传感器技术与数据分析
  • 【SRE】安装Grafana实践