C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C语言 递归宏

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 完成“结构性多次播放”;
在需要“变长/嵌套遍历”的地方,用**递归宏(延迟展开)**把“列表加工”补齐。
落地后,增删命令就只改一行,枚举、字符串、解析器保持同步。

七、边界与避坑清单(工程经验浓缩)

九、再给两个“像真递归”的小玩具

计数:编译期统计参数个数

#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 */)

到此这篇关于C语言递归宏的具体使用的文章就介绍到这了,更多相关C语言 递归宏内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文