PAYATHON 2026

如何在 C 宏中拼接标识符并生成标签?

数据阿明

结论

问题出在 DEFER_ID 并没有保存固定编号,它只是 __COUNTER__ 的别名。每次展开 DEFER_ID,计数器都会生成一个新值,所以 DF_RETURN 和 DF_END 拼出的标签可能不一致,导致跳转无法匹配。两层宏 CAT 的写法是正确的;相关宏需要使用同一个编号。

让跳转使用相同的编号

可以把编号作为参数传给定义结束标签和执行返回的宏。两层拼接宏会先展开参数,再执行 ##:

#define CAT_(a_, b_) a_##b_
#define CAT(a_, b_) CAT_(a_, b_)
#define DEFER_LABEL(id_) CAT(defer_, id_)

#define DF_BEGIN(return_, value_, id_) \
    return_ return__ = (value_); \
    struct defer_list defer_list_ = { .size = 0, .list = {} }

#define DF_END(id_) \
    DEFER_LABEL(id_): \
    if (defer_list_.size != 0) \
        goto *(defer_list_.list[--defer_list_.size]); \
    return return__;

#define DF_RETURN(return_, id_) \
    return__ = (return_); \
    goto DEFER_LABEL(id_)

同一组宏调用要传入相同的编号:

DF_BEGIN(int, 0, 17);

/* 注册延迟执行项 */
DF_RETURN(1, 17);

DF_END(17);

这里用 17 作示例。实际代码中,每个使用这些宏的作用域都要选用一致的编号,并确保它不会与同一函数内的其他标签冲突。

为什么两层拼接宏有用

直接使用 a_##b 拼接时,如果参数本身是宏,通常不会先展开。先调用 CAT,再由 CAT_ 执行拼接,就能让参数先展开。例如:

#define CAT_(a_, b_) a_##b_
#define CAT(a_, b_) CAT_(a_, b_)
#define LABEL(id_) CAT(defer_, id_)

LABEL(17) 会生成标识符 defer_17。这种写法解决了拼接时的参数展开顺序,但无法让分别展开的 __COUNTER__ 自动得到相同的值。

__COUNTER__ 与编译器扩展

__COUNTER__ 每次展开时都会递增。因此,DF_RETURN 和 DF_END 分别展开 DEFER_ID 时,无法依靠它们自动共享编号。若要用计数器生成编号,需要通过一种能将编号传递给所有相关宏调用的设计来捕获并传递它。单独写 #define DEFER_ID __COUNTER__ 无法完成捕获。

此外,示例中的 &&label、goto *expression 和 ({ ... }) 都是 GNU C 扩展,不属于标准 C 语法。请确认项目的编译模式允许这些扩展;如果要求严格符合标准 C,应改用标准 C 支持的控制流和资源清理设计。

还应给 defer_list 增加容量检查:size 是 uint8_t,数组有 256 个元素。注册项过多时,可能发生越界或计数回绕。

备注:内容仅供参考。