生长中技术笔记
C 语言的"泛型模板":宏拼接与 _Generic 两种实现
C 语言没有 C++ 那样的模板机制,但在实际开发中经常需要"同一套逻辑、不同数据类型"的写法。本文介绍两种常见的模拟手段:一种靠宏的 token 拼接伪造出类似 Pair<int, char> 的语法,另一种利用 C11 的 _Generic 关键字按参数类型分发到不同函数。两者各有适用场景和明显局限,下面分别说明。
一、通过宏定义模拟模板语法
核心思路是利用 ## 运算符把类型名拼进标识符,从而为每种类型组合生成独立的 struct 定义:
cpp
原理与限制
Pair(T1, T2)展开后是Pair$_int_$$_char_$这样的标识符,宏本身只做文本拼接,不提供模板约束;展开后生成的结构体和字段仍会由 C 编译器做普通类型检查。- 只能传入类型名(如
int、char、Str),不能传入表达式或带*的指针类型(int*中的*会破坏 token 拼接)。 - 每新增一种类型组合,就必须多写一行
Pair_Def(...),没有真正的"自动实例化"。 - 示例拼出的标识符含
C 语言没有 C++ 那样的模板机制,但在实际开发中经常需要"同一套逻辑、不同数据类型"的写法。本文介绍两种常见的模拟手段:一种靠宏的 token 拼接伪造出类似Pair<int, char>的语法,另一种利用 C11 的_Generic` 关键字按参数类型分发到不同函数。两者各有适用场景和明显局限,下面分别说明。
一、通过宏定义模拟模板语法
核心思路是利用 ## 运算符把类型名拼进标识符,从而为每种类型组合生成独立的 struct 定义:
cpp
原理与限制
Pair(T1, T2)展开后是Pair$_int_$$_char_$这样的标识符,宏本身只做文本拼接,不提供模板约束;展开后生成的结构体和字段仍会由 C 编译器做普通类型检查。- 只能传入类型名(如
int、char、Str),不能传入表达式或带*的指针类型(int*中的*会破坏 token 拼接)。 - 每新增一种类型组合,就必须多写一行
Pair_Def(...),没有真正的"自动实例化"。
,而C 语言没有 C++ 那样的模板机制,但在实际开发中经常需要"同一套逻辑、不同数据类型"的写法。本文介绍两种常见的模拟手段:一种靠宏的 token 拼接伪造出类似Pair<int, char>的语法,另一种利用 C11 的_Generic` 关键字按参数类型分发到不同函数。两者各有适用场景和明显局限,下面分别说明。
一、通过宏定义模拟模板语法
核心思路是利用 ## 运算符把类型名拼进标识符,从而为每种类型组合生成独立的 struct 定义:
cpp
原理与限制
Pair(T1, T2)展开后是Pair$_int_$$_char_$这样的标识符,宏本身只做文本拼接,不提供模板约束;展开后生成的结构体和字段仍会由 C 编译器做普通类型检查。- 只能传入类型名(如
int、char、Str),不能传入表达式或带*的指针类型(int*中的*会破坏 token 拼接)。 - 每新增一种类型组合,就必须多写一行
Pair_Def(...),没有真正的"自动实例化"。
不是 ISO C 保证支持的标识符字符,只能在确认工具链扩展后使用。调试器中看到的也是拼接后的名称。
适合场景:类型组合有限且固定(比如只有两三种),希望调用处看起来接近 C++ 模板写法。
二、通过 _Generic 关键字实现类型分发
_Generic 是 C11(ISO/IEC 9899:2011)引入的关键字,语法为:
text
_Generic( type_expr, type1 : assoc1, type2 : assoc2, ..., default : assocN )编译时,编译器根据控制表达式经语言规则处理后的类型选择对应的 assoc 分支(这是编译期选择,不是预处理展开)。下面用一个 min 函数演示基本用法:
cpp
用 _Generic 实现 swap
把 min 的思路套到 swap 上,可以写成:
cpp
#define swap(a, b) _Generic((a), \
int : swap_int, \
float : swap_float, \
default : swap_double \
)(a, b)
void swap_int(int *a, int *b) { /* ... */ }
void swap_float(float *a, float *b) { /* ... */ }
void swap_double(double *a, double *b) { /* ... */ }调用示例:
cpp
int a = 1;
double b = 1.1;
swap(a, b); // 调用 swap_int(a, b)
float c = 1.1;
int d = 1;
swap(c, d); // 调用 swap_float(c, d)分发规则
_Generic只看第一个参数(即type_expr)的类型来决定走哪个分支,后续参数不参与选择。- 上例中
a是int,命中int : swap_int;c是float,命中float : swap_float。 default分支是兜底,类型未显式列出时走这里。
需要注意的问题
- 类型必须精确匹配。
_Generic比较的是类型本身,int和long、float和double是不同分支。如果传入unsigned int而列表里只有int,会落入default(或编译报错,取决于是否有default)。 - 值类型 vs 指针类型。 上例中
swap_int的签名是void swap_int(int *a, int *b),但调用处写的是swap(a, b)(传值而非传地址)。如果实际编译,这里会产生类型不匹配的警告或错误。实际使用时应写成swap(&a, &b),并且_Generic判断的也是int *而非int。上面的示例代码仅作概念演示,直接编译大概率无法通过。 - 指针可以匹配,数组要谨慎。 关联列表可明确写
int * : ...或具体函数指针类型。数组在控制表达式中通常会经过类型转换,不应假设int[10]一定能按数组类型匹配。 - 编译器支持。 需要 C11 或更高语言模式;具体版本和兼容性以项目所用编译器文档为准。
- 没有真正的泛型。 每个分支仍然是独立的函数,逻辑重复时无法像 C++ 模板那样只写一份。
三、两种方案对比
| 维度 | 宏 token 拼接 | _Generic |
|---|---|---|
| 标准 | C89/C99 即可 | C11 起 |
| 分发依据 | 无(纯文本) | 第一个参数的实际类型 |
| 类型检查 | 展开后按普通 C 代码检查 | 可在编译期选分支,但仍要检查函数实参 |
| 适用对象 | 类型名(struct 定义) | 函数/表达式选择 |
| 扩展成本 | 每种组合手写一行 | 每种类型加一个 case |
小结
C 语言没有模板,但宏拼接和 _Generic 可以在有限场景下提供"按类型走不同路径"的能力。选择时建议:
- 需要为不同类型组合定义结构体 → 宏拼接更直接。
- 需要按参数类型选择不同函数 →
_Generic更清晰,且编译器能帮你检查类型。 - 类型组合多、逻辑复杂 → 认真考虑是否该用 C++,或者把通用逻辑抽成
void *+ 显式类型标签的方案。
无论哪种方式,都建议在项目文档中注明"这不是真正的泛型",避免后续维护者误以为可以随意传入任意类型。
<!-- csdn-article-id: 131108038 -->本文最初于 2023/6/8 发布在 CSDN。
评论
正在读取评论…