函数重载是 C++ 的招牌功能,C 语言没有。但 C11 标准加了 _Generic 关键字,让你能按参数类型在编译期选不同的表达式,配合宏用,就能实现类似重载的效果。下面看它怎么用。
_Generic 是什么
_Generic 是一个编译期的选择表达式:它检查控制表达式(controlling expression)的类型,然后从后面的类型列表中挑一个匹配的返回。它很像 switch,只是 switch 按值选分支,_Generic 按类型选分支。
_Generic( (expression),
type_1 : statements,
type_2 : statements,
...
default : statements
)关键点:它是编译期决定的,控制表达式不会被真正求值,没被选中的分支也不会执行。
第一个例子:按类型返回数字
// C 程序演示 _Generic 按类型选择
#include <stdio.h>
int main(void) {
// _Generic 像个 switch,按参数的数据类型选择结果
printf("%d\n", _Generic(1.0L, float : 1, double : 2,
long double : 3, default : 0));
printf("%d\n", _Generic(1L, float : 1, double : 2,
long double : 3, default : 0));
printf("%d\n", _Generic(1.0L, float : 1, double : 2,
long double : 3));
return 0;
}输出:
3
0
3第一个 1.0L 是 long double,匹配 long double 返回 3;1L 也是 long,但注意 1L 的类型是 long,不是 long double,所以走 default 返回 0;第三个没写 default,而 1.0L 正好匹配 long double,所以仍返回 3。
在宏里用:实现类似函数重载
宏的参数没有类型检查,一个宏可能被喂进各种类型。用 _Generic 给宏加类型感知,让不同类型返回不同结果,这就是 C 版的函数重载:
#include <stdio.h>
// 定义宏:为不同参数类型返回不同字符串
#define typeName(T) _Generic((T), \
char* : "String", \
int : "Integer", \
long : "Long Integer", \
default : "Others")
int main(void) {
// A 是字符串字面量,类型为 char*
printf("%s\n", typeName("A"));
// 5 是 int
printf("%s\n", typeName(5));
// 5.12 是 float,宏里没定义,走 default
printf("%s", typeName(5.12));
return 0;
}输出:
String
Integer
Others同一个宏,传字符串返回"String"、传整数返回"Integer"、传没定义的类型返回"Others"。行为和 C++ 函数重载按参数类型挑版本很接近。
典型应用:类型名称宏与数学函数分发
一个经典用法是类型自省,返回参数类型的名字:
#include <stdio.h>
#define TYPE_NAME(X) _Generic((X), \
int: "int", \
char: "char", \
double: "double", \
default: "unknown")
int main(void) {
printf("Type name: %s\n", TYPE_NAME(42));
printf("Type name: %s\n", TYPE_NAME(42.42));
printf("Type name: %s\n", TYPE_NAME('c'));
return 0;
}输出:
Type name: int
Type name: double
Type name: int注意最后一个:'c' 是字符常量,但在 C 语言里字符常量是 int 类型,所以匹配 int。这正是 _Generic 对 lvalue 转换的规则。
标准库的 tgmath.h 也靠 _Generic 干活,把 cos() 调用按参数类型映射到 cosf()(float)、cos()(double)或 ccos()(复数),程序员写一个名字,编译器自动选对版本:
// 类似 tgmath.h 的实现思路
#define cbrt(X) _Generic((X), \
long double: cbrtl, \
default: cbrt, \
float: cbrtf \
)(X)
#include <math.h>
#include <stdio.h>
int main(void) {
double x = 8.0;
const float y = 3.375;
// 选出 double 版本 cbrt()
printf("cbrt(8.0) = %f\n", cbrt(x));
// const float 转成 float,选出 cbrtf()
printf("cbrtf(3.375) = %f\n", cbrt(y));
return 0;
}输出:
cbrt(8.0) = 2.000000
cbrtf(3.375) = 1.500000这里 _Generic 只负责选出函数指针,函数调用参数列表放在宏的末尾 (X) 上,这样参数只在真正调用的那一刻求值,不会在匹配阶段被误算。
使用规则与限制
- 同一个类型不能在列表里出现两次。
- 不能同时列出兼容类型(比如枚举和它的底层整型)。
- 没有 default 时,控制表达式的类型必须恰好匹配列表中的一个类型,否则编译报错。
- 控制表达式和未被选中的分支都不会被求值,所以不怕副作用。
- 匹配前会去掉 const、volatile、restrict 限定符,也会做 lvalue 转换(比如字符串字面量按 char* 匹配)。
优缺点
优点:
- 配合宏能模拟函数重载,一份代码处理多种类型。
- 按类型分发,能写出类型安全的宏。
缺点:
- 主要用在宏里,宏本身容易出错、调试困难。
- 语法复杂,宏展开后不好阅读,出错时定位麻烦。
_Generic 是 C11 给 C 语言的编译期类型分派。单个看有点绕,但配合宏,它能在不引入 C++ 的前提下,解决很多不同类型要做不同事的麻烦。