函数重载是 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++ 的前提下,解决很多不同类型要做不同事的麻烦。