感觉自己一直对C++的宏展开没有细致地研究过,这两天深入地学习了一下,做个笔记。
文章目录
首先明确宏展开,是在预处理阶段进行的,进入编译期就是宏展开之后的代码了,所以不会有什么编译期的类型检查。
宏展开基本规则
基础语法
cpp
#define identifier replacement-list(optional)
#define identifier(parameters) replacement-list(optional)
#define identifier(parameters, ...) replacement-list(optional) (since C++11)
#define identifier(...) replacement-list(optional) (since C++11)
#undef identifier
前两个比较好理解,第一个宏展开类型位 object-like,后面的宏展开类型是 function-like,下面主要说明下可变参数的情况。
在宏定义中,替换列表中可以使用 __VA_ARGS_identifier
标识符访问可变参数。
举几个例子就清晰了
cpp
#define F(...) f(__VA_ARGS__)
#define G(X, ...) f(0, X, ##__VA_ARGS__)
F(1, 2, 3); // f(1, 2, 3);
G(1); // f(0, 1);
这里 G(X, ...)
里 f(0, X, ##__VA_ARGS__)
中的 ##__VA_ARGS__
是用来解决当 X
后面没有其它参数了,去除掉多余的 ,
,注意在 C++20 中引入 __VA_OPT__
宏来更规范地代替 ##__VA_ARGS__
,但我在MSVC里测试还不支持,查了资料 Clang 和 GCC 都是支持的。
此外还有两个常用的运算符 #
和 ##
,#
使传入的实参变成字符串,##
使得传入的两个实参连接(concatenate),具体还是看两个例子
cpp
#define CHAR_(c) #c
#define CONBINE_(a, b) a##b
CHAR_(1223) // "1223"
CONBINE_(12, 23) // 1223
CHAR_(CONBINE_(12, 23)) // "CONBINE_(12, 23)"
最后一行涉及到宏嵌套,放到下一节细说。
宏嵌套展开
宏嵌套就是宏的形参可以是一个宏,形成一个层层嵌套的关系,宏嵌套展开的流程图如下:
这里的两个条件分别是:
- 条件1:当前宏体中是否含有
#
或##
对应上一节最后提到的情况 - 条件2:当前是否是最内层
这里涉及到宏定义不能递归展开,所以实际要通过两个标记位实现:
- replacing bit:这个标记位标识宏当前是否正在被它的替换列表替代,下面用rb简称
- unavailable bit:这个标记位表示token是否还能被宏展开,下面用ub简称
比如传入一个token T,其具体的宏展开流程如下:
- 初始token T的rb和ub都是清0的
- 判断T是否能宏展开:
2.1 如果 T 是宏,且其rb为1,cpp设置 T 的ub为1
2.2 如果 T 是object-like宏 且 ub 为0,T可以宏展开
2.3 如果 T 是function-like宏 且 ub 为0,且 T后面有(
,T可以宏展开 - 如果 T 不能宏展开,就添加到当前输出token列表。如果能宏展开,进行两阶段的宏展开:
3.1 T 是一个function-like宏,cpp 扫描提供给 T 的所有实参,如果实参也是宏,要对实参尝试进行宏展开。实参宏展开流程和T宏展开流程一致,都是先判断再展开,只是 T 展开完的结果是放入主预处理输出,而实参的输出则是放入对T每个实参独有的替换token列表。也会记住实参展开前的原始宏,因为 T 中可能有用#
或##
连接,需要这个原始宏,而不是宏展开之后的结果。
3.2 T 如果有实参,cpp会使用T的替换列表,来用宏展开完全的实参token list替换T中的形参。也会执行#
和##
的字符串化和粘贴。然后,它逻辑上将生成的token添加到输入列表的前面。最后,cpp将名为T的宏的rb设置为true。 - rescan
T的rb现在为真,cpp继续处理添加到输入列表的tokens(是从这个位置开始继续检查宏展开,和前面的没关系)。这可能会产生更多的宏展开。一旦cpp已经使用了由替换列表生成的所有tokens,预处理器会清0rb
还是举几个例子
例1
cpp
#define FL(x) ((x)+1) // function-like macro
FL(FL(5)) // => ((((5)+1))+1)
我们按照上面的流程分析一下,首先输入的 token T为 FL
,其初始rb和ub都是清0的,
然后 FL(FL(5))
明显是一个 function-like 的宏,且ub为0,且FL
后面有(
,那么可以宏展开。
那么首先对其实参尝试进行宏展开,FL(5)=>((5)+1)
,产生的token就是((5)+1)
,此时FL的rb设为1,然后再用实参展开的结果替换形参,即FL(((5)+1))=>(((5)+1)+1)
。
例2
cpp
#define ID(arg) arg
ID(ID)(ID)(X) // => ID(ID)(X)
输入的 token T是 ID
,明显是一个 function-like 宏,且ub为0,后面有(
,可以宏展开。
对其实参先进行尝试宏展开,实参不能宏展开,那么替换列表里的token就是 ID
,替换掉形参,即
ID(ID)=>ID
然后设置 ID
的rb为1,进行rescan,ID
的rb为1,其ub设为1,发现虽然 ID
还是一个function-like,但其ub被设为 1 了,所以不能再宏展开了,括号里面的由于没有 (
跟随所以也不能宏展开。
例3
cpp
#define EMPTY
#define SCAN(x) x
#define EXAMPLE_() EXAMPLE
#define EXAMPLE(n) EXAMPLE_ EMPTY()(n-1) (n)
EXAMPLE(5) // => EXAMPLE_()(5 - 1) (5)
SCAN(EXAMPLE(5)) // => EXAMPLE_()(5 - 1 - 1)(5 - 1)(5)
对于 EXAMPLE(5)
其是一个 function-like 的宏,实参也不能宏展开,那么直接替换 EXAMPLE_ EMPTY()(5-1) (5)
然后rescan,从前往后扫,遇到 EMPTY
宏 替换,最后结果就为 EXAMPLE_()(5 - 1) (5)
。
而对于 SCAN(EXAMPLE(5))
其也是一个function-like宏,实参可以宏展开,最终结果就是上面的 EXAMPLE_()(5 - 1)(5)
,然后实参替换掉形参,那么需要rescan,EXAMPLE_()
宏可以替换为 EXAMPLE
,然后 EXAMPLE(5 - 1)
可以替换为 EXAMPLE_ EMPTY()(5 - 1 - 1)(5 - 1)
然后,rescan 发现 EMPTY
可以替换,然后最终结果就为 EXAMPLE_()(5 - 1 - 1)(5 - 1)(5)
。
补充说明
补充1:宏定义多行写可以用 \
换行
cpp
#define FL(x) \
((x)+1)
补充2:宏定义会对传入字符串进行适当地处理,下面假如反斜杠防止多个""
cpp
#define STR(s) #s
STR("123") // "\"123\""
参考资料
Recursive macros with C++20 VA_OPT
Replacing text macros
可视化 C/C++ 宏扩展
深入聊一聊C/C++中宏展开过程
聊一聊新的宏__VA_OPT__
C语言复杂宏展开的替换顺序及定义查看