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 实现的能力。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。