找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖
Claude、GPT 海外模型 API 接入Claude skills 从入门到精通 吴恩达亲授 AI Agent 核心技能2026 瞪哥公务员考试全攻略 行测申论一站式系统备考
Agent 文心智能蒸馏模型实战 90G 课程智泊 AI 大模型训练营 基于 LangChain 的 RAG 与提示工程实战构建企业级 AI 大脑:大模型微调与 RAG / Agent 全栈实战

5011

积分

0

好友

645

主题
发表于 2 小时前 | 查看: 6| 回复: 0

在平常写代码时,宏用得不算多。一方面不够熟悉,另一方面也担心用出问题。所以很长一段时间里,我对宏的理解都停留在下面这种:

#define MAX(a, b) ((a) > (b) ? (a) : (b))

前几天读代码时碰到了一堆宏,脑子里忽然冒出个念头:能不能用宏来实现 for 循环?比如像下面这样:

FOR_EACH(PRINT, a, b, c)

希望展开后得到:

PRINT(a) 
PRINT(b) 
PRINT(c)

最初的想法其实不复杂:每次取出一个参数处理,然后继续处理剩下的参数。按照递归的思路,代码写出来也很简洁:

#define FOR_EACH(action, first, ...) \ action(first) \ FOR_EACH(action, __VA_ARGS__)

第一次取出 a,剩下 b, c;第二次取出 b,剩下 c;最后处理 c,结束。

逻辑上看起来很顺,但有个硬伤:C++ 的宏并不能递归,至少不能像函数那样递归。

你可能会好奇,宏为什么不能像函数那样递归?先看这个最简单的例子:

#define RECURSE(x) RECURSE(x)

如果使用 RECURSE(10),按照普通函数的思路,RECURSE(10) 展开之后还是 RECURSE(10)。这样反复下去,预处理器岂不是永远也停不下来?

其实,预处理器在展开一个宏时,会阻止这个宏在当前这条展开链中再次被直接展开。也就是说,当 RECURSE(10) 第一次被展开后,虽然结果里又出现了 RECURSE(10),但它不会继续像普通递归函数那样调用自己。

这也是前面 FOR_EACH 行不通的原因:

#define FOR_EACH(action, first, ...) \

    action(first) \

    FOR_EACH(action, __VA_ARGS__)

第一次处理 a 之后,我们希望得到:

PRINT(a)
FOR_EACH(PRINT, b, c)

然后继续处理 b、c,可宏的展开规则不允许我们这么直接干。

所以想实现 FOR_EACH,至少得解决两个问题:

  • 怎么知道参数已经处理完了?
  • 既然宏不能直接递归,怎么让下一次 FOR_EACH 继续展开?

针对第一个问题,可以使用 C++20 中的 __VA_OPT__。

它的规则很简单:如果 __VA_ARGS__ 中有参数,就展开括号里的内容;如果没有参数,就什么都不产生。比如:

#define LOG(fmt, ...) \

    printf(fmt __VA_OPT__(,) __VA_ARGS__)

调用 LOG("hello") 时,因为没有额外参数,__VA_OPT__(,) 什么都不会产生,最终得到 printf("hello");而调用 LOG("%d", 10) 时,因为存在额外参数,__VA_OPT__(,) 会产生一个逗号,最终得到 printf("%d", 10)。

至此,第一个问题解决了。剩下的问题更麻烦:怎么让一个不能直接递归的宏,再次调用自己?

既然 FOR_EACH 不能在展开过程中直接再次展开自己,那不妨换个思路:不要让下一次调用马上出现,而是等预处理器后面再次扫描时,再让它出现。

先定义:

#define PARENS ()

#define FOR_EACH_AGAIN() FOR_EACH_HELPER

然后写真正处理参数的宏:

#define FOR_EACH_HELPER(action, a1, ...) \

    action(a1) \

    __VA_OPT__(FOR_EACH_AGAIN PARENS (action, __VA_ARGS__))

这个思路本质上就是延迟调用。不过这种方案存在一个问题:只会调用一次。

比如 FOR_EACH_AGAIN PARENS (PRINT, b, c),可以变成 FOR_EACH_HELPER(PRINT, b, c),但这只解决了“下一次调用怎么重新出现”的问题。想继续处理 b、c,后面还会再次产生新的延迟调用:

PRINT(b)
FOR_EACH_AGAIN PARENS (PRINT, c)

也就是说,参数有多少个,这种“延迟 → 重新出现 → 再延迟”的过程就可能重复多少次。

最简单的办法,就是再套一层宏:

#define EXPAND1(...) __VA_ARGS__

一层不够,就继续套:

#define EXPAND2(...) \

    EXPAND1(EXPAND1(EXPAND1(EXPAND1(__VA_ARGS__))))

再往上继续:

#define EXPAND3(...) \

    EXPAND2(EXPAND2(EXPAND2(EXPAND2(__VA_ARGS__))))

#define EXPAND4(...) \

    EXPAND3(EXPAND3(EXPAND3(EXPAND3(__VA_ARGS__))))

#define EXPAND(...) \

    EXPAND4(EXPAND4(EXPAND4(EXPAND4(__VA_ARGS__))))

完整的代码如下:

#define PARENS ()

#define EXPAND(...) \
    EXPAND4(EXPAND4(EXPAND4(EXPAND4(__VA_ARGS__))))

#define EXPAND4(...) \
    EXPAND3(EXPAND3(EXPAND3(EXPAND3(__VA_ARGS__))))

#define EXPAND3(...) \
    EXPAND2(EXPAND2(EXPAND2(EXPAND2(__VA_ARGS__))))

#define EXPAND2(...) \
    EXPAND1(EXPAND1(EXPAND1(EXPAND1(__VA_ARGS__))))

#define EXPAND1(...) __VA_ARGS__

#define FOR_EACH_HELPER(action, a1, ...) \
    action(a1) \
    __VA_OPT__(FOR_EACH_AGAIN PARENS (action, __VA_ARGS__))

#define FOR_EACH_AGAIN() FOR_EACH_HELPER

#define FOR_EACH(action, ...) \
    __VA_OPT__(EXPAND(FOR_EACH_HELPER(action, __VA_ARGS__)))

为了验证,再定义一个最简单的 PRINT:

#define PRINT(x) std::cout << x << '\n';

现在:

FOR_EACH(PRINT, a, b, c)

最终就会得到:

std::cout << a << '\n';
std::cout << b << '\n';
std::cout << c << '\n';

回过头再看整个过程,其实就是:取一个参数 → 处理 → 判断还有没有参数 → 延迟下一次调用 → 继续展开 → 再处理一个参数,直到所有参数全部处理完。

严格来说,上面的做法并不是真正的递归,只是看起来像递归罢了。

普通函数递归时,下一次调用会在运行过程中动态发生,调用多少次取决于运行时的终止条件。而上面的宏不一样:我们通过 EXPAND 提前准备了足够多的展开层级,再利用 PARENS 和 FOR_EACH_AGAIN,让被延迟的宏在后续扫描中一层层展开。

所以它并没有真正实现“宏调用自己”,而是提前铺好足够多的展开机会,再让每一次被延迟的调用依次出现。

从这个角度看,所谓“递归宏”本质上是一种有上限的递归模拟。EXPAND 提供了多少展开空间,这套写法最多就能向下展开多深。

以上




上一篇:微软悄悄弃用 Copilot+ PC 品牌:新款 Surface 不再提及
下一篇:千万级向量检索优化:Milvus HNSW索引、过滤与重排序实战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-9-27 03:25 , Processed in 1.111869 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表