在 C 中使用新标签兼容性规则的参数化类型

  • 2025 年 6 月 26 日,nullprogram.com/blog/2025/06/26/ 提到 C23 为 struct、union 和 enum 兼容性有新规则,从今年 4 月发布的 GCC 15 开始以及今年晚些时候的 Clang 开始在编译器中出现。

    • 不同翻译单元(TU)中定义的相同 struct 一直是兼容的,这是它们工作的基础,之前每个 TU 中的定义是不同的、不兼容的类型,新规则改变了这一情况,它们是兼容的,这解锁了一些使用宏的类型参数化。
  • 之前 TU 中不能有 struct 的多个定义是因为作用域问题,例如:

    struct Example { int x, y, z; };
    
    struct Example example(void)
    {
      struct Example { int x, y, z; };
      return (struct Example){1, 2, 3};
    }
  • 可以通过宏改变之前的写法,如:

    #define Slice(T)        \
      struct Slice##T {   \
          T        *data; \
          ptrdiff_t len;  \
          ptrdiff_t cap;  \
      }

    这样可以在需要时动态生成 slice 类型,例如:

    Slice(int) range(int, Arena *);
    
    float mean(Slice(float));
    
    Slice(Str) split(Str, char delim, Arena *);
    Str join(Slice(Str), char delim, Arena *);

    或者在模型解析器中使用:

    typedef struct {
      float x, y, z;
    } Vec3;
    
    typedef struct {
      int32_t v[3];
      int32_t n[3];
    } Face;
    
    typedef struct {
      Slice(Vec3) verts;
      Slice(Vec3) norms;
      Slice(Face) faces;
    } Model;
    
    typedef Slice(Vec3) Polygon;
  • 但这些宏可能会让工具困惑,如 Universal Ctags 看不到带有 slice 类型的字段,不过它们类似于非常有限的 C++模板,新的技术与通用函数无关,而通用 slice 函数可以弥补新技巧的不足,例如:

    typedef struct { char *beg, *end; } Arena;
    void *alloc(Arena *, ptrdiff_t count, int size, int align);
    
    #define push(a, s)                          \
    ((s)->len == (s)->cap                     \
     ? (s)->data = push_(                    \
          (a),                                \
          (s)->data,                          \
          &(s)->cap,                          \
          sizeof(*(s)->data),                 \
          _Alignof(typeof(*(s)->data))        \
        ),                                    \
        (s)->data + (s)->len++                \
      : (s)->data + (s)->len++)
    
    void *push_(Arena *a, void *data, ptrdiff_t *pcap, int size, int align)
    {
      ptrdiff_t cap = *pcap;
    
      if (a->beg!= (char *)data + cap*size) {
          void *copy = alloc(a, cap, size, align);
          memcpy(copy, data, cap*size);
          data = copy;
      }
    
      ptrdiff_t extend = cap? cap : 4;
      alloc(a, extend, size, align);
      *pcap = cap + extend;
      return data;
    }

    可以利用新的标签规则和即将到来的 C2y 空指针规则编写类似这样的代码:

    Slice(int64_t) generate_primes(int64_t limit, Arena *a)
    {
      Slice(int64_t) primes = {};
    
      if (limit > 2) {
          *push(a, &primes) = 2;
      }
    
      for (int64_t n = 3; n < limit; n += 2) {
          bool valid = true;
          for (ptrdiff_t i = 0; valid && i<primes.len; i++) {
              valid = n % primes.data[i];
          }
          if (valid) {
              *push(a, &primes) = n;
          }
      }
    
      return primes;
    }
  • 但也存在局限性,比如定义 Map(K, V) 时没有通用函数来操作它就没什么意义,而且 Slice##T 要求宏的参数是标识符,需要逐步构建,这有点违背了方便的目的。例如:

    typedef Slice(float) Edges;
    
    typedef struct {
      Slice(Str)   names;
      Slice(Edges) edges;
    } Graph;

    虽然好处不大,但值得研究,作者还写了一个小演示 demo.c 供查看和测试本地 C 实现的能力。

阅读 23
0 条评论