COUNT 自动计数宏

在嵌入式 C 工程里,经常需要「每展开一次宏就得到一个不同的整数」或「拼出不会重名的符号」。GCC / Clang / MSVC 等编译器提供内置宏 __COUNTER__;项目里常再包一层 _COUNT_,统一命名、便于检索,也避免和业务里的 COUNT 之类名字冲突。

核心定义

/* count.h — 建议在公共头里只定义一次 */
#ifndef COUNT_H
#define COUNT_H

/* 每次预处理展开 _COUNT_,得到 0、1、2、… 递增的整型字面量 */
#define _COUNT_ __COUNTER__

/* 与标识符拼接:生成唯一符号名 */
#define _PASTE_(a, b)  a##b
#define _PASTE(a, b)   _PASTE_(a, b)
#define _UNIQUE_(prefix) _PASTE(prefix, _COUNT_)

#endif

展开示例:

int a = _COUNT_;   /* 0 */
int b = _COUNT_;   /* 1 */
int c = _COUNT_;   /* 2 */

__COUNTER__ 行为要点

特性 说明
起始值 每个翻译单元(.c 文件)从 0 开始
递增时机 每次宏被展开时加 1,与运行时无关
__LINE__ 对比 __LINE__#include、行号变化,同一行可重复;_COUNT_ 在同一 TU 内按展开次序单调递增,更适合生成唯一名
未使用的实参 作为宏实参传入但未被使用时,部分编译器不会递增(以实现为准)
# 字符串化、## 粘贴 多数编译器在未真正展开为独立 token 时不递增
预编译头 若 PCH 里已展开过 __COUNTER__,再包含可能影响计数连续性,需注意 PCH 与宏混用

标准 C 尚未正式纳入 __COUNTER__(C23 提案 P3384),但 GCC、Clang、IAR、Keil ARMCC/ARMClang、MSVC(较新版本) 在嵌入式场景里普遍可用;移植前用目标工具链各写一行 _COUNT_ 验证即可。

典型用法

1. 生成唯一的静态对象名(注册表、单例表)

同一宏在多个 .c 里展开时,若都放在函数外的全局/静态作用域,需要全局唯一符号名:

#define REGISTER_DRIVER(drv) \
    static const driver_t _UNIQUE_(drv_inst_) = (drv); \
    /* 链接段或构造函数里把 &drv_inst_x 登记进表 */

REGISTER_DRIVER(driver_uart);
REGISTER_DRIVER(driver_spi);
/* 展开为 drv_inst_0、drv_inst_1 … */

TensorFlow 的 REGISTER_KERNEL_BUILDER、部分 RTOS / 协议栈的「自动注册」宏,本质都是 __COUNTER__ + ## 这一套。

2. 头文件内联函数里的「局部」唯一变量

C 里不能在函数内用 static 做「每次调用不同名字」;用 _COUNT_宏展开点固定一个展开序号,再交给第二层宏生成符号:

#define _DEFER_(x)  x
#define _EXPAND_(x) _DEFER_(x)

#define FOR_EACH_ITEM(item, code) \
    _EXPAND_(FOR_EACH_ITEM_IMPL(_COUNT_, item, code))

#define FOR_EACH_ITEM_IMPL(n, item, code) \
    enum { _UNIQUE_(fe_idx_) = n }; \
    code

/* 多次包含同一宏列表时,n 不同,避免重复定义 */

3. 与 X-Macro 配合:自动槽位 / 端点号

枚举或配置表希望「写一行宏就占一个连续编号」,又不想手写 0,1,2…

#define EP_LIST \
    X(uart) \
    X(spi)  \
    X(i2c)

typedef enum {
#define X(name) EP_##name = _COUNT_,
    EP_LIST
#undef X
    EP_MAX
} ep_id_t;

USB/QMK 等固件里用 __COUNTER__ 自动分配 endpoint number 是同类思路。

4. 调试:区分多次展开的静态断言

#define STATIC_ASSERT_MSG(cond, msg) \
    typedef char _UNIQUE_(static_assert_) [(cond) ? 1 : -1]

同一文件里多处断言时,错误信息不会都挤在同一个 typedef 名上(配合 _Static_assert 更推荐,但老编译器仍用此法)。

完整小例子

#include "count.h"

#define TABLE_ENTRY(name, fn) \
    { .id = _COUNT_, .name = #name, .handler = fn }

static const struct {
    unsigned id;
    const char *name;
    void (*handler)(void);
} cmd_table[] = {
    TABLE_ENTRY(reset,  cmd_reset),
    TABLE_ENTRY(status, cmd_status),
    TABLE_ENTRY(version, cmd_version),
};
/* id 自动为 0, 1, 2,无需手写 */

可移植兜底(无 __COUNTER__ 时)

极老编译器没有 __COUNTER__ 时,只能退化为 __LINE__(同一行重复展开会撞名)或放弃自动计数、改用手写枚举:

#if defined(__COUNTER__)
#  define _COUNT_ __COUNTER__
#elif defined(__LINE__)
#  define _COUNT_ __LINE__
#else
#  define _COUNT_ 0   /* 失去自动递增,仅作占位 */
#endif

作用域与包含关系(重要限制)

_COUNT_ 的计数范围是翻译单元(TU),不是「某个 .c 文件」或「某个头文件」各自一份。#include 只是把头文件文本插入当前 TU 的预处理流程,不会为头文件单独开计数器。

同一 .c 与其包含的头文件:共享一条计数链

/* foo.h */
#define DECLARE(x)  int _UNIQUE_(obj_) = (x);

/* main.c */
#include "foo.h"

DECLARE(1);       /* _COUNT_ → 0,生成 obj_0 */
int a = _COUNT_;  /* → 1,不是 0 */
DECLARE(2);       /* → 2,生成 obj_2 */

预处理顺序是:先展开头文件里已经出现的 _COUNT_,再展开 .c 里后面的 _COUNT_,数字连续递增,不会在进 .c 主体时重新从 0 开始。

场景 是否共享同一 _COUNT_ 序列
同一 .c 与其 #include 的头文件
头文件被多个不同 .c 各自包含 否,每个 .c 各自从 0 计
头文件有 include guard,只被粘贴一次 该头内 _COUNT_ 只消耗一次,但仍计入包含它的那个 .c 的序列

不同 .c 之间:互不影响

每个 .c(及它展开的全部头文件)是独立 TU,各自从 0 起算。不能假定 module_a.c 里用到 _COUNT_ == 3 时,module_b.c 里下一个也是 4;链接后符号名、枚举值若依赖跨文件连续编号,必须改用手写枚举或集中在一个 .c 里生成。

容易踩坑的情况

  1. 头文件里直接写 _COUNT_(例如在头里 REGISTER_XXX() 或带 _UNIQUE_ 的静态对象):所有包含该头的 .c 都会在各自 TU 内先消耗一段编号;同一 .c 里若后面还有命令表、X-Macro 列表,得到的下标会从偏大处继续,不是从 0 起。
  2. 以为「只在 .c 里用就从 0 开始」:若前面 include 的公共头、驱动注册宏已经展开过 _COUNT_.c 里第一次 _COUNT_ 可能已是 5、10 等。
  3. 同一宏调用里多次 _COUNT_:一次调用内写两次会得到两个不同数字;需要「只递增一次」时,应先把 _COUNT_ 作为实参传给子宏(两层宏,见下文「使用注意」)。
  4. include guard 只防止头被重复粘贴;第一次包含时其中的 _COUNT_ 仍会正常计数。

推荐组织方式

  • 定义 _COUNT_ / _UNIQUE_ 的宏放在公共头;实际展开REGISTER_xxx(...)、X-Macro 列表)尽量放在 .c
  • 需要「从 0 连续编号」的表(命令表、端点表):在单个 .c 内集中写宏列表并展开,且避免在此之前 include 会消耗 _COUNT_ 的头;或把这类头放在 include 顺序靠后、仅该 .c 使用的私有头里。
  • 头文件只提供宏定义,由调用方在 .c 里写 REGISTER_DRIVER(uart),计数发生在调用处,语义更清晰。
flowchart LR
  subgraph TU["一个翻译单元 main.c"]
    H["#include foo.h\n_DECLARE → _COUNT_=0"]
    C1["main.c DECLARE(1) → 1"]
    C2["main.c _COUNT_ → 2"]
    H --> C1 --> C2
  end
  subgraph TU2["另一个翻译单元 other.c"]
    O["#include foo.h 从 0 重新计"]
  end

使用注意

  1. 不要指望 _COUNT_ 在运行时变化;它是编译期常量,可用于 enum、数组长度、case 标签(若值为常量表达式)。

  2. 同一宏调用里多次写 _COUNT_ 会得到不同数字;若需要「一次调用只递增一次」,应先把 _COUNT_ 传给子宏的形参,再在子宏里只用该形参拼接名字:

    #define REGISTER(x)  REGISTER_IMPL(_COUNT_, x)
    #define REGISTER_IMPL(n, x)  static obj_t obj_##n = (x)
    
  3. 命名:项目统一用 _COUNT_ 前缀,避免与业务宏 COUNT(x)、计数器变量 count 混淆;_COUNT_ 定义头加 include guard。

小结

_COUNT_ 通常就是 __COUNTER__ 的项目别名:每展开一次加 1,配合 ## 生成唯一标识符、自动表项下标、注册宏实例名。嵌入式里写驱动注册表、命令表、X-Macro 枚举时很实用。务必记住:同一 .c 与其 include 的头共享计数,不同 .c 互不影响;写宏时用「先捕获计数再展开」的两层宏,并确认目标链支持 __COUNTER__