用宏生成代码:C 预处理器的 #、## 与 X-Macro

大多数人用宏只会 #define 替换,但预处理器还有两个操作符几乎没人注意到,用好了能省掉大量重复代码。


#:字符串化

在宏参数前加 #,把参数原样变成字符串字面量:

#define STRINGIFY(x) #x

STRINGIFY(hello)      // → "hello"
STRINGIFY(123)        // → "123"
STRINGIFY(a + b)      // → "a + b"

最实用的场景是调试打日志时自动带上变量名:

#define LOG_VAR(x) printf(#x " = %d\n", (x))

int voltage = 3300;
LOG_VAR(voltage);  // 输出:voltage = 3300

不用手写字符串,变量改名时日志自动跟着变。


##:符号拼接

把两个符号拼接成一个新符号:

#define REG(n) reg_##n

REG(status)   // → reg_status
REG(ctrl)     // → reg_ctrl

常见于批量生成函数名或变量名,避免手写大量相似代码:

#define DEFINE_SETTER(field, type) \
    void set_##field(type val) { g_config.field = val; }

DEFINE_SETTER(brightness, uint8_t)   // 展开为 void set_brightness(uint8_t val) {...}
DEFINE_SETTER(volume,     uint8_t)   // 展开为 void set_volume(uint8_t val) {...}

X-Macro:把两者推到极致

有了 ### 的基础,来看一个真正解决实际问题的技巧。

问题场景:定义一组错误码,同时需要一个函数把错误码转成字符串:

// 常见写法——枚举和字符串表分开维护
typedef enum {
    ERR_OK,
    ERR_TIMEOUT,
    ERR_BUSY,
    ERR_NO_MEM,
} error_t;

static const char *err_names[] = {
    "ERR_OK",
    "ERR_TIMEOUT",
    "ERR_BUSY",
    "ERR_NO_MEM",
};

每次新增一个错误码,要改两个地方。漏改一个,字符串表就对不上,而且编译器不会报错。

X-Macro 写法

用一个宏列表作为唯一数据源,枚举和字符串表都从它生成:

// 唯一数据源
#define ERROR_LIST \
    X(ERR_OK,      "Success")       \
    X(ERR_TIMEOUT, "Timeout")       \
    X(ERR_BUSY,    "Busy")          \
    X(ERR_NO_MEM,  "Out of memory")

// 生成枚举
typedef enum {
    #define X(code, str) code,
    ERROR_LIST
    #undef X
} error_t;

// 生成字符串表
static const char *err_str[] = {
    #define X(code, str) [code] = str,
    ERROR_LIST
    #undef X
};

// 转换函数
const char *error_to_str(error_t e) {
    if (e < sizeof(err_str) / sizeof(err_str[0])) return err_str[e];
    return "Unknown";
}

新增错误码只改 ERROR_LIST 一行,枚举、字符串表、顺序全部自动同步,物理上不可能不一致。


还能生成什么

X-Macro 不限于枚举+字符串,同一张表可以同时生成多种产物:

#define CMD_LIST        \
    X(CMD_START,  0x01, do_start,  "start")  \
    X(CMD_STOP,   0x02, do_stop,   "stop")   \
    X(CMD_RESET,  0x03, do_reset,  "reset")

// 生成枚举
typedef enum {
    #define X(name, code, fn, str) name = code,
    CMD_LIST
    #undef X
} cmd_t;

// 生成函数指针表(结合上一篇的技巧)
typedef void (*cmd_fn_t)(void);
static const struct { uint8_t code; cmd_fn_t fn; const char *name; } cmd_table[] = {
    #define X(name, code, fn, str) { code, fn, str },
    CMD_LIST
    #undef X
};

命令码、处理函数、名称字符串,三者永远绑在一起。


注意事项

  • #undef X 每次用完必须加,否则下次重定义 X 会产生警告或冲突
  • 宏展开没有类型检查,错误提示可能指向展开后的行,不好定位
  • 适合数量多、结构规整、需要多种形式的数据;逻辑复杂的场景不要强行套
请登录后发表评论

    没有回复内容