在平常写代码时,宏用得不算多。一方面不够熟悉,另一方面也担心用出问题。所以很长一段时间里,我对宏的理解都停留在下面这种:
#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 提供了多少展开空间,这套写法最多就能向下展开多深。
以上