X-Macro

作者:Administrator 发布时间: 2026-06-03 阅读量:2 评论数:0

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                          // <--- 用完销毁,方便下次重定义

预处理器是如何工作的?

  1. 预处理器看到 #define X(name, val) name = val
  2. 进入 ALL_GUIDE_RULES(X)
  3. 遇到第一行数据 X(STYLE_ITEM_USED_TYPEDEF, 0x1001)
  4. 根据刚才定义的 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";
    }
}

预处理器是如何工作的?

  1. 这次 X 被定义为:case name: return #name;
  2. #name 进行字符串拼接,如果 nameABC,那么 #name 就会变成字符串 "ABC"
  3. 再次进入 ALL_GUIDE_RULES(X)
  4. 遇到第一行,将其替换为: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";
    }
}

总结

想象一下,如果你不使用这个技巧,而是手动写代码。

普通写法:

  1. 你在 enum 里加了一个新规则 NEW_RULE
  2. 忘记GetRuleName 函数里加对应的 case
  3. 程序运行时,如果你打印这个 ID,它会返回 "UNKNOWN" 或者崩溃,得花大量时间排查

X-Macro 写法:

  1. 你只需要在 第 2 步 的列表里加一行: X(NEW_RULE, 0x1003)
  2. 编译时,枚举定义会自动增加这一项
  3. 同时,switch-case 也会自动增加这一行代码
  4. 绝对不会出现“枚举有了,但字符串函数里没写”的情况

Macro List (X-Macro): 将数据与逻辑分离,数据存放在宏列表中,逻辑通过传入不同的宏参数 X 来实现。

评论