王剑编程网

分享专业编程知识与实战技巧

C语言进阶教程:C11新特性:泛型选择 (_Generic)

C语言传统上是一种静态类型语言,缺乏像C++模板或Java泛型那样直接的泛型编程机制。然而,C11标准引入了一个称为泛型选择 (Generic Selection) 的新特性,通过关键字 _Generic 实现。它允许程序员根据控制表达式的类型,在编译时选择多个表达式中的一个来执行。这提供了一种在C语言中实现类型相关的行为的方式,常用于创建类型安全的宏或函数。

_Generic的基本语法

_Generic 是一个一元运算符,其语法结构如下:

 _Generic ( controlling-expression ,
            type-name1 : expression1 ,
            type-name2 : expression2 ,
            ...
            default    : expressionN
          )
  • controlling-expression:控制表达式。它的类型(在移除任何顶层限定符如 const, volatile, restrict 之后)将与后续的 type-name 进行匹配。
  • type-nameX:一个具体的类型名(如 int, double*, char[5] 等)。
  • expressionX:与 type-nameX 关联的表达式。如果 controlling-expression 的类型匹配 type-nameX,则整个 _Generic 表达式的结果就是 expressionX
  • default (可选):如果 controlling-expression 的类型与前面列出的所有 type-name 都不匹配,则选择 default 关联的表达式。如果没有 default 分支且没有类型匹配,则程序无法通过编译。

重要特性:

  1. 编译时选择_Generic 的选择过程在编译时完成。controlling-expression 本身不会被求值(除非它被选中的 expressionX 所使用)。
  2. 结果是表达式:整个 _Generic(...) 结构本身是一个表达式,其类型和值由被选中的 expressionX 决定。
  3. 类型匹配:类型匹配是精确的,包括数组类型和函数指针类型。类型限定符(const, volatile, restrict)在匹配前会被移除。

简单示例

 #include <stdio.h>
 
 // 定义一个宏,根据参数类型打印不同的信息
 #define print_type_info(X) printf( \ 
     _Generic((X), \ 
         int: "Argument is an int: %d\n", \ 
         double: "Argument is a double: %f\n", \ 
         char*: "Argument is a char*: %s\n", \ 
         const char*: "Argument is a const char*: %s\n", \ 
         default: "Argument is of an unknown type\n" \ 
     ), (X) \ 
 )
 
 int main() {
     int i = 10;
     double d = 3.14;
     char* s = "Hello";
     const char* cs = "World";
     float f = 2.71f;
 
     print_type_info(i);
     print_type_info(d);
     print_type_info(s);
     print_type_info(cs);
     print_type_info(f); // 将匹配 default
 
     // _Generic 本身是一个表达式,可以赋值
     char* type_str = _Generic(i, int: "Integer", default: "Other");
     printf("Type string for i: %s\n", type_str);
 
     return 0;
 }

输出:

 Argument is an int: 10
 Argument is a double: 3.140000
 Argument is a char*: Hello
 Argument is a const char*: World
 Argument is of an unknown type
 Type string for i: Integer

print_type_info 宏中:

  • _Generic((X), ...) 根据 X 的类型选择一个格式字符串。
  • 选中的格式字符串随后作为 printf 的第一个参数。
  • (X)printf 的末尾作为实际要打印的值。

使用 _Generic实现类型安全的宏

_Generic 最常见的用途之一是创建行为类似于函数重载的宏,根据参数类型调用不同的函数。

示例:类型安全的 abs

标准库 <stdlib.h> 提供了 abs (for int), labs (for long), llabs (for long long),<math.h> 提供了 fabs (for double), fabsf (for float), fabsl (for long double)。 我们可以使用 _Generic 创建一个统一的 generic_abs 宏。

 #include <stdio.h>
 #include <stdlib.h> // for abs, labs, llabs
 #include <math.h>   // for fabs, fabsf, fabsl
 
 #define generic_abs(X) _Generic((X), \ \
     int: abs(X), \ \
     long int: labs(X), \ \
     long long int: llabs(X), \ \
     float: fabsf(X), \ \
     double: fabs(X), \ \
     long double: fabsl(X), \ \
     default: (X) /* 或者抛出编译错误,或默认行为 */ \ \
 )
 
 int main() {
     int i = -5;
     long l = -100L;
     long long ll = -1000LL;
     float f = -3.14f;
     double d = -2.718;
     long double ld = -0.12345L;
 
     printf("abs(%d) = %d\n", i, generic_abs(i));
     printf("abs(%ld) = %ld\n", l, generic_abs(l));
     printf("abs(%lld) = %lld\n", ll, generic_abs(ll));
     printf("abs(%f) = %f\n", f, generic_abs(f));
     printf("abs(%f) = %f\n", d, generic_abs(d));
     printf("abs(%Lf) = %Lf\n", ld, generic_abs(ld));
 
     // char c = -10; // 会匹配 default,因为 char 不是上面列出的类型
     // printf("abs(%d) = %d\n", (int)c, generic_abs(c));
     // 如果希望char也处理,可以添加 char: abs((int)X) (因为abs接受int)
 
     return 0;
 }

这个 generic_abs 宏会根据传入参数 X 的类型,在编译时选择调用相应版本的绝对值函数。

_Generic与函数指针

_Generic 的结果是一个表达式,这个表达式可以是一个函数指示符(function designator)。这意味着你可以用 _Generic 来选择一个函数,然后像调用普通函数一样调用它,或者获取其地址。

 #include <stdio.h>
 
 void func_int(int x) { printf("Called func_int with %d\n", x); }
 void func_double(double x) { printf("Called func_double with %f\n", x); }
 void func_default(void* x) { printf("Called func_default\n"); }
 
 #define CALL_APPROPRIATE_FUNC(X) _Generic((X), \ \
     int: func_int, \ \
     double: func_double, \ \
     default: func_default \ \
 )(X)
 
 int main() {
     int i = 42;
     double d = 3.14;
     char c = 'a';
 
     CALL_APPROPRIATE_FUNC(i);    // 实际调用 func_int(i)
     CALL_APPROPRIATE_FUNC(d);    // 实际调用 func_double(d)
     CALL_APPROPRIATE_FUNC(c);    // 实际调用 func_default(c)
 
     // 获取函数指针
     void (*ptr_to_func_for_i)(int);
     ptr_to_func_for_i = _Generic(i, int: func_int, default: NULL);
     if (ptr_to_func_for_i) {
         ptr_to_func_for_i(i);
     }
 
     return 0;
 }

CALL_APPROPRIATE_FUNC(X) 宏中:

  1. _Generic((X), ...) 根据 X 的类型选择 func_int, func_double, 或 func_default
  2. 选中的函数名后面紧跟着 (X),构成一个函数调用表达式。

类型匹配的细节

  • 精确匹配:类型匹配必须是精确的。例如,intlong int 是不同的类型。
  • 限定符移除:在进行类型匹配之前,控制表达式的顶层类型限定符(const, volatile, restrict)会被移除。
  • const int ci = 10;
    // _Generic(ci, int: ...) // 会匹配 int
  • 数组与指针:数组类型在传递给函数时通常会退化为指针。但在 _Generic 的控制表达式中,数组类型本身会被保留用于匹配(除非它先发生了退化)。
  • int arr[5];
    // _Generic(arr, int[5]: "array of 5 ints", int*: "pointer to int", default: ...)
    // 这里会匹配 int[5]

    int *ptr = arr;
    // _Generic(ptr, int[5]: ..., int*: "pointer to int", default: ...)
    // 这里会匹配 int*
  • 函数类型:函数名在表达式中通常会退化为函数指针。_Generic 可以匹配函数类型或函数指针类型。

_Generic的优点

  1. 类型安全:与传统的C宏相比,_Generic 允许基于类型做出决策,从而可以编写更类型安全的宏。
  2. 编译时决策:选择在编译时完成,没有运行时开销。
  3. 代码可读性:对于实现类型相关的行为,_Generic 比复杂的预处理器技巧或多个 if-else 结构(基于运行时类型信息,如果可能的话)更清晰。
  4. 减少代码重复:可以为相似操作的不同类型版本提供统一的接口。

_Generic的局限性

  1. 不是真正的泛型编程_Generic 只是一个编译时的选择机制。它不能像C++模板那样自动为新类型生成代码。你必须为每个期望支持的类型显式地提供一个分支。
  2. 冗余:如果需要支持很多类型,_Generic 表达式可能会变得很长很冗余。
  3. 控制表达式不求值:虽然通常是优点(避免副作用),但也意味着不能根据控制表达式的来做选择,只能根据其类型
  4. C11标准_Generic 是C11标准引入的。如果需要兼容C99或更早的C标准,则不能使用它。
  5. 复杂类型:对于非常复杂的类型(如多级指针、复杂结构体等),编写 type-name 可能会很繁琐。

何时使用 _Generic?

  • 创建类型安全的宏:这是最主要的应用场景,例如前面演示的 generic_abs 或日志宏,根据参数类型选择不同的格式化或处理函数。
  • 模拟函数重载:为一组功能相似但操作于不同类型的函数提供一个统一的调用接口。
  • 在编译时分派到特定实现:根据某个对象的类型,选择不同的处理逻辑。

示例:更复杂的类型匹配

 #include <stdio.h>
 
 struct MyStruct { int x; };
 
 const char* get_type_name(void* p) {
     return _Generic(p, // p 是 void*, 但我们通常会传递具体类型的指针
         int*: "pointer to int",
         double*: "pointer to double",
         struct MyStruct*: "pointer to MyStruct",
         void*: "pointer to void (or default for pointers)",
         default: "not a recognized pointer type or not a pointer"
     );
 }
 
 // 注意:上面的 get_type_name 实际上是根据 p 的类型 (void*) 来选择的,
 // 这可能不是我们想要的。如果我们想根据指针指向的类型来选择,
 // 我们需要传递指针本身,而不是 void*。
 // 或者,如果传递的是对象本身,可以解引用指针(如果知道它非NULL)。
 
 #define TYPE_NAME(X) _Generic((X), \ \
     int: "int", \ \
     int*: "int*", \ \
     const int*: "const int*", \ \
     int[5]: "array of 5 ints", \ \
     int[]: "array of unknown-bound ints (incomplete type)", \ \
     void (*)(void): "function pointer void(void)", \ \
     default: "other type" \ \
 )
 
 void my_func(void) {}
 
 int main() {
     int i;
     int *pi = &i;
     const int *cpi = &i;
     int arr5[5];
     // int arr_incomplete[]; // 不能直接声明不完整类型的对象
 
     printf("Type of i: %s\n", TYPE_NAME(i));
     printf("Type of pi: %s\n", TYPE_NAME(pi));
     printf("Type of cpi: %s\n", TYPE_NAME(cpi)); // const被移除,匹配 int*
     printf("Type of arr5: %s\n", TYPE_NAME(arr5));
     printf("Type of my_func: %s\n", TYPE_NAME(my_func)); // 函数名退化为函数指针
 
     return 0;
 }

输出 (示例,const int* 的情况):

Type of i: int
Type of pi: int*
Type of cpi: int* 
Type of arr5: array of 5 ints
Type of my_func: function pointer void(void)

注意 TYPE_NAME(cpi) 的结果是 int* 而不是 const int*,因为 _Generic 在匹配前会移除顶层 const 限定符。如果想区分 const int*int*_Generic 本身做不到,因为 const 是在类型匹配规则中被剥离的。

总结

_Generic 是C11引入的一个有用的特性,它提供了一种在编译时根据表达式类型进行选择的机制。虽然它不是一个完整的泛型编程解决方案,但它极大地增强了C语言在创建类型安全的宏和实现类型相关行为方面的能力。

通过 _Generic,开发者可以编写更清晰、更健壮、更易于维护的代码,尤其是在需要处理多种数据类型但希望提供统一接口的场景中。然而,使用时也需要注意其C11标准的依赖性以及它并非万能的泛型工具这一事实。

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言