CPP 技巧-X Macro

PointY Lv2

基础知识

预处理展开主要指 C/C++ 中‌宏(Macro)替换‌的机制,核心遵循“由内向外、参数预扫描、防递归”原则。‌‌

预处理核心展开流程

  1. ‌参数预扫描(Prescan)‌:在将实参替换入宏体前,先对实参进行完全展开。若实参本身是宏,则先展开为最终值,再代入。
  2. ‌宏体替换与再扫描‌:将展开后的实参替换进宏定义主体,然后对结果重新扫描,继续展开其中包含的其他宏,直到无可展开宏为止。
  3. ‌禁止直接递归‌:若宏在展开过程中再次遇到自身名称,预处理器会标记该宏为“正在展开”,停止进一步展开以防止无限循环。‌‌

特殊运算符的例外规则

当宏参数涉及以下运算符时,‌参数预扫描被抑制‌,参数保持原貌:

  • ‌#(字符串化)‌:参数不被展开,直接转换为字符串字面量。例如 #x 会将 x 转为 "x",即使 x 是宏也不展开。
  • ‌##(令牌粘贴)‌:参数不被展开,直接与相邻令牌合并为新标识符。合并后的结果若构成新宏,会在后续扫描中展开。

‌注意‌:若参数未受 ### 保护,即使包含逗号也不视为参数分隔符,而是作为整体参与展开。‌‌

关键执行顺序

  1. ‌文件包含优先‌:#include 在宏展开前处理,确保被包含文件中的宏定义可用。
  2. ‌条件编译依赖宏值‌:#if 等指令评估前需完成相关宏的展开,以获取正确表达式值。
  3. ‌单次扫描逻辑‌:预处理大致为单次扫描过程,宏展开产生的文本不会重新解析为新的预处理指令(如 #define)。‌‌

常见陷阱

  • ‌优先级错误‌:函数式宏体及参数建议加括号,如 #define SQUARE(x) ((x)*(x)),避免 SQUARE(1+2) 展开为 1+2*1+2=5 而非 9
  • ‌间接展开技巧‌:若需对 ### 的参数进行展开,需通过两层宏定义(如 STRINGIFY(x) 调用 STRINGIFY_HELPER(x))利用两步展开机制实现。‌‌

可通过编译器选项(如 GCC 的 -E)查看预处理后的完整代码以验证展开结果。‌

X Macro

先定义一个包含多条数据的统一列表,然后通过临时改变 X 的含义,在不同上下文里重复使用这个列表,自动生成不同的代码,比如枚举、字符串数组、switch 分支等。

其核心思想是通过 #defineundef 控制宏的作用范围,使得 X 宏在不同的地方有不同的表达。

优缺点

优点

  • 单一数据源:所有关联信息(如名称、缩写、数值)集中在一处,修改时只需改一个地方。
  • 避免重复:不会因为多拷贝同一组数据而出现不一致。
  • 维护性强:新增或删除一项数据后,所有自动生成的代码都会同步更新,减少遗漏。

缺点

  • 可读性下降:对不熟悉 X Macro 的人来说,代码比较晦涩。
  • 调试困难:预处理后的代码难以直接查看,编译错误信息可能指向展开后的位置,不直观。
  • IDE 支持差:代码跳转、自动补全等通常对宏展开的支持有限。

示例:实现枚举转字符串

1
2
3
4
5
6
7
8
// 这里定义一个宏,X 只是占位符,具体使用时通过 #define 定义
#define LOG_LEVELS \
X(Trace, T) \
X(Debug, D) \
X(Info, I) \
X(Warning, W) \
X(Error, E) \
X(Fatal, F)

1
2
3
4
5
6
enum class LogLevel : uint8_t
{
#define X(name, short) name,
LOG_LEVELS
#undef X
};

展开为

1
2
3
4
5
6
7
8
9
enum class LogLevel : uint8_t
{
Trace,
Debug,
Info,
Warning,
Error,
Fatal
};

1
2
3
4
5
6
7
8
9
10
11
12
13
#define STRINGIFY(x) #x
constexpr const char *LogLevelNames[] = {
#define X(name, short) STRINGIFY(name),
LOG_LEVELS
#undef X
};

constexpr const char *LogLevelShortNames[] = {
#define X(name, short) STRINGIFY(short),
LOG_LEVELS
#undef X
};
#undef STRINGIFY

这里定义 STRINGIFY 宏,是为了 先展开实参,再字符串化

如果直接写成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
constexpr const char *LogLevelNames[] = {
#define X(name, short) #name,
LOG_LEVELS
#undef X
};

// 将会先被替换为 ①
constexpr const char *LogLevelNames[] = {
#define X(name, short) #name,
X(Trace, T) \
X(Debug, D) \
X(Info, I) \
X(Warning, W) \
X(Error, E) \
X(Fatal, F)
#undef X
};

// 预期是被展开为 ②
constexpr const char *LogLevelNames[] = {
"Trace",
"Debug",
"Info",
"Warning",
"Error",
"Fatal",}

⚠ 但是如果后期拓展

1
2
3
4
5
6
7
8
9
10
#define MIN_LOG_LEVEL Trace
#define MAX_LOG_LEVEL Fatal

#define LOG_LEVELS \
X(MIN_LOG_LEVEL, T)
X(Debug, D) \
X(Info, I) \
X(Warning, W) \
X(Error, E) \
X(MAX_LOG_LEVEL, F)

那么 ① 将被转换为 ③

1
2
3
4
5
6
7
8
9
10
constexpr const char *LogLevelNames[] = {
#define X(name, short) #name,
X(MIN_LOG_LEVEL, T) \
X(Debug, D) \
X(Info, I) \
X(Warning, W) \
X(Error, E) \
X(MAX_LOG_LEVEL, F)
#undef X
};

再展开为 ④

1
2
3
4
5
6
7
constexpr const char *LogLevelNames[] = {
"MIN_LOG_LEVEL",
"Debug",
"Info",
"Warning",
"Error",
"MAX_LOG_LEVEL",}

这样就不符合预期

使用 STRINGIFY 宏可以解决这个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#define STRINGIFY(x) #x
constexpr const char *LogLevelNames[] = {
#define X(name, short) STRINGIFY(name),
LOG_LEVELS
#undef X
};
#undef STRINGIFY

// 将会先被替换为 ①
#define STRINGIFY(x) #x
constexpr const char *LogLevelNames[] = {
#define X(name, short) STRINGIFY(name),
X(MIN_LOG_LEVEL, T) \
X(Debug, D) \
X(Info, I) \
X(Warning, W) \
X(Error, E) \
X(MAX_LOG_LEVEL, F)
#undef X
};
#undef STRINGIFY

// 继续展开为 ②
#define STRINGIFY(x) #x
constexpr const char *LogLevelNames[] = {
STRINGIFY(MIN_LOG_LEVEL) \
STRINGIFY(Debug) \
STRINGIFY(Info) \
STRINGIFY(Warning) \
STRINGIFY(Error) \
STRINGIFY(MAX_LOG_LEVEL)
};
#undef STRINGIFY

// 这里没用直接触碰 `#` `##`
// 检查宏表发现 MIN_LOG_LEVEL 、 MAX_LOG_LEVEL是已定义的宏,进行替换为 ③
#define STRINGIFY(x) #x
constexpr const char *LogLevelNames[] = {
STRINGIFY(Trace) \
STRINGIFY(Debug) \
STRINGIFY(Info) \
STRINGIFY(Warning) \
STRINGIFY(Error) \
STRINGIFY(Fatal)
};
#undef STRINGIFY

// 展开STRINGIFY 为 ④
constexpr const char *LogLevelNames[] = {
"Trace",
"Debug",
"Info",
"Warning",
"Error",
"Fatal",}

实现将枚举值转为字符串的接口

1
2
3
4
5
6
7
8
9
inline const char *LeveltoString(LogLevel level)
{
return LogLevelNames[static_cast<uint8_t>(level)];
}

inline const char *LeveltoShortString(LogLevel level)
{
return LogLevelShortNames[static_cast<uint8_t>(level)];
}
  • 标题: CPP 技巧-X Macro
  • 作者: PointY
  • 创建于 : 2026-07-02 20:59:00
  • 更新于 : 2026-07-15 14:39:29
  • 链接: https://siyuhong.github.io/2026/07/02/cppskill-xmacro/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论