X-Macro
X-Macro(X 宏)技术核心目的是:维护一份数据(单一数据源),同时生成枚举和字符串之间的映射
讲人话:当有很多很多枚举的时候,我希望能够打印出枚举的字符串,而不是 0,1,2,3…
问题背景
以代码规范检查为例,用下面的枚举表示若干个检查类型:
enum RuleId {
STYLE_ITEM_USED_TYPEDEF = 0x1001,
STYLE_ITEM_NOT_USE_IF_ZERO = 0x1002,
SECURE_ITEM_DONOT_USE_ABORT = 0x1003,
...
};
我希望统计一段代码违反哪些规范,当输出的时候希望有类似 STYLE_ITEM_USED_TYPEDEF 这样的输出而不是输出 0x1001,前者更加直观,后者还需要去代码中查表才知道每个数字表示的含义。
想解决这样的问题可以在枚举之外手动将每个枚举进行 switch case 转化为字符串,类似这样:
switch (ruleId) {
case STYLE_ITEM_USED_TYPEDEF: return "STYLE_ITEM_USED_TYPEDEF";
case STYLE_ITEM_NOT_USE_IF_ZERO: return "STYLE_ITEM_NOT_USE_IF_ZERO";
case SECURE_ITEM_DONOT_USE_ABORT: return "SECURE_ITEM_DONOT_USE_ABORT";
....
default: return "UNKNOWN";
}
这种方式将相同的数据重复写了一遍,这会导致一个问题:当宏非常多,想要新增或删除一些宏的时候,很容易漏掉一些导致出错,这时候就可以使用 X-Macro 技术。
X-Macro
代码实现
// Step 1. 定义数据列表 (X-Macro List)
// 这是唯一需要维护的地方
#define ALL_GUIDE_RULES(X) \
X(STYLE_ITEM_USED_TYPEDEF, 0x1001) \
X(STYLE_ITEM_NOT_USE_IF_ZERO, 0x1002) \
X(SECURE_ITEM_DONOT_USE_ABORT, 0x1003)
// Step 2. 第一遍展开:生成枚举
#define X(name, val) name = val,
enum GuideRuleId {
ALL_GUIDE_RULES(X)
};
// 取消 X 宏定义,因为后面要重新定义 X 宏
#undef X
// Step 3. 第二遍展开:生成字符串函数
std::string GetRuleName(GuideRuleId id) {
switch (id) {
#define X(name, val) case val: return #name;
ALL_GUIDE_RULES(X)
#undef X
default: return "UNKNOWN";
}
}
int main() {
GuideRuleId id1 = STYLE_ITEM_USED_TYPEDEF;
GuideRuleId id2 = SECURE_ITEM_DONOT_USE_ABORT;
cout << "ID: " << id1 << " Name: " << GetRuleName(id1) << endl;
cout << "ID: " << id2 << " Name: " << GetRuleName(id2) << endl;
return 0;
}
// 输出如下:
ID: 4097 Name: STYLE_ITEM_USED_TYPEDEF
ID: 8193 Name: SECURE_ITEM_DONOT_USE_ABORT
Step 1
// Step 1. 定义数据列表 (X-Macro List)
#define ALL_GUIDE_RULES(X) \
X(STYLE_ITEM_USED_TYPEDEF, 0x1001) \
X(STYLE_ITEM_NOT_USE_IF_ZERO, 0x1002) \
X(SECURE_ITEM_DONOT_USE_ABORT, 0x1003)
X 是一个参数,可以把 ALL_GUIDE_RULES(X) 当成一个函数,它接受一个叫 X 的操作动作,然后对列表里的每一行数据都执行一次 X 动作。
此时 X 有定义吗? 还没有!我们稍后会在不同的地方定义 X 是什么,从而产生不同的代码。
Step 2
// Step 2. 第一遍展开:生成枚举
#define X(name, val) name = val, // <--- 这里的 X 定义了如何生成枚举项
enum GuideRuleId {
ALL_GUIDE_RULES(X) // <--- 在这里调用大宏
};
#undef X // <--- 用完销毁,方便下次重定义
预处理器是如何工作的?
- 预处理器看到
#define X(name, val) name = val - 进入
ALL_GUIDE_RULES(X) - 遇到第一行数据
X(STYLE_ITEM_USED_TYPEDEF, 0x1001) - 根据刚才定义的
X,将其替换为:STYLE_ITEM_USED_TYPEDEF = 0x1001
最终编译器生成的代码为:
enum GuideRuleId {
STYLE_ITEM_USED_TYPEDEF = 0x1001,
STYLE_ITEM_NOT_USE_IF_ZERO = 0x1002,
SECURE_ITEM_DONOT_USE_ABORT = 0x1003,
};
Step 3
这是 X-Macro 最强大的地方,我们重用了那份数据列表,但是重新定义了 X。
// Step 3. 第二遍展开:生成字符串函数
std::string GetRuleName(GuideRuleId id) {
switch (id) {
// 下面这行定义了 X 的新行为:生成 case 语句
// #name 是 C 语言的“字符串化”操作符,把 tokens 变成 "字符串"
#define X(name, val) case name: return #name;
ALL_GUIDE_RULES(X) // <--- 再次调用大宏
#undef X // <--- 再次销毁
default: return "UNKNOWN";
}
}
预处理器是如何工作的?
- 这次
X被定义为:case name: return #name; #name进行字符串拼接,如果name是ABC,那么#name就会变成字符串"ABC"- 再次进入
ALL_GUIDE_RULES(X) - 遇到第一行,将其替换为:
case STYLE_ITEM_USED_TYPEDEF: return "STYLE_ITEM_USED_TYPEDEF";
最终编译器生成的代码为:
std::string GetRuleName(GuideRuleId id) {
switch (id) {
case STYLE_ITEM_USED_TYPEDEF: return "STYLE_ITEM_USED_TYPEDEF";
case STYLE_ITEM_NOT_USE_IF_ZERO: return "STYLE_ITEM_NOT_USE_IF_ZERO";
case SECURE_ITEM_DONOT_USE_ABORT: return "SECURE_ITEM_DONOT_USE_ABORT";
default: return "UNKNOWN";
}
}
总结
想象一下,如果你不使用这个技巧,而是手动写代码。
普通写法:
- 你在
enum里加了一个新规则NEW_RULE - 你忘记在
GetRuleName函数里加对应的case - 程序运行时,如果你打印这个 ID,它会返回
"UNKNOWN"或者崩溃,得花大量时间排查
X-Macro 写法:
- 你只需要在 第 2 步 的列表里加一行:
X(NEW_RULE, 0x1003) - 编译时,枚举定义会自动增加这一项
- 同时,
switch-case也会自动增加这一行代码 - 绝对不会出现“枚举有了,但字符串函数里没写”的情况
Macro List (X-Macro): 将数据与逻辑分离,数据存放在宏列表中,逻辑通过传入不同的宏参数 X 来实现。