C++宏展开

感觉自己一直对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,其具体的宏展开流程如下:

  1. 初始token T的rb和ub都是清0的
  2. 判断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可以宏展开
  3. 如果 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。
  4. 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语言复杂宏展开的替换顺序及定义查看

相关推荐
Good kid.几秒前
王者荣耀改重复名(java源码)
java·开发语言
奶糖趣多多3 分钟前
类的继承性和多态性
java·开发语言
图像处理大大大大大牛啊21 分钟前
使用mingw64 编译 QT开发流程
开发语言·c++·qt·命令模式
爱上杨小厨32 分钟前
MFC实现对话框与控件的自适应调节
c++·mfc
计算机学姐39 分钟前
基于python+django+vue的农业管理系统
开发语言·vue.js·后端·python·django·pip·web3.py
洪大宇41 分钟前
Windows Python 指令补全方法
开发语言·python
惜.己1 小时前
基于Spring搭建SpringMvc框架
java·开发语言·后端·spring·tomcat·mvc·idea
爱技术的小伙子1 小时前
【30天玩转python】面向对象编程基础
开发语言·python
Bill661 小时前
MFC之CString类及其成员函数用法详解
c++·mfc
theoxiong1 小时前
Python的Scapy库详解
开发语言·python·网络协议·tcp/ip·http·信息与通信·scapy