12.1 泛型选择
泛型选择 (Generic Selection) 是 C11 标准引入的一个新特性,它根据表达式的类型来选择不同的值。通过 _Generic 关键字实现。
1. 基本语法
_Generic 的基本语法如下:
_Generic(表达式,
类型1: 值1,
类型2: 值2,
default: 默认值
)2
3
4
5
当编译器对 _Generic 表达式求值时,它会:
- 确定括号中表达式的类型
- 在类型列表中查找匹配项
- 如果找到匹配的类型,返回对应的值
- 如果没有找到匹配项,返回
default对应的值
2. 实际示例
#include <stdio.h>
#define TYPE_NAME(x) _Generic((x), \
int: "整数", \
double: "双精度浮点数", \
char: "字符", \
default: "未知类型" \
)
int main() {
int i = 42;
double d = 3.14;
char c = 'A';
char* s = "Hello World!";
printf("i 的类型是:%s\n", TYPE_NAME(i));
printf("d 的类型是:%s\n", TYPE_NAME(d));
printf("c 的类型是:%s\n", TYPE_NAME(c));
printf("s 的类型是:%s\n", TYPE_NAME(s));
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
输出:
i 的类型是:整数 d 的类型是:双精度浮点数 c 的类型是:字符 s 的类型是:未知类型
3. 实用场景
_Generic 最常见的用途是实现类型泛型的宏。下面用一个简单的类型名称打印宏演示核心思路:
#include <stdio.h>
#define PRINT_TYPE(x) printf("类型:%s\n", _Generic((x), \
int: "整数", \
double: "双精度浮点数", \
float: "单精度浮点数", \
default: "未知类型" \
))
int main(void) {
int i = 42;
double d = 3.14;
float f = 2.71f;
PRINT_TYPE(i);
PRINT_TYPE(d);
PRINT_TYPE(f);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
更复杂的场景(如根据类型分派到不同函数)需要第 6 章已经介绍的函数定义语法。
4. 习题
下面这段程序用 _Generic 判断类型名称:
#include <stdio.h>
#define KIND(x) _Generic((x), \
char: "char", \
int: "int", \
unsigned: "unsigned", \
float: "float", \
double: "double", \
int *: "int *", \
char *: "char *", \
default: "其他" \
)
int main(void) {
char c = 'A';
const int ci = 2;
short s = 3;
int arr[4] = {0};
printf("%s\n", KIND(c));
printf("%s\n", KIND(ci));
printf("%s\n", KIND(s));
printf("%s\n", KIND(arr));
printf("%s\n", KIND("abc"));
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(1) [2.2] 不运行,写出五行输出,并说明 ci 与 "abc" 各匹配到哪一个分支。
(2) [2.2] 只删去 default 一行,重新编译。哪一行最先报错,报的是什么?说明 short 与 int 在 _Generic 里为什么不算同一种类型。
(3) [3.1] 在 main 之前加上:
static int calls;
static int bump(void) {
++calls;
return 1;
}2
3
4
5
把最后一行 printf("%s\n", KIND("abc")); 改成 printf("%s\n", KIND(bump()));。运行后 calls 是多少?说明 _Generic 对括号里的表达式做了什么、没做什么。
实现一个按实参类型选择绝对值的宏:
#define MDR_ABS(x) /* 补全 */要求:
int、long、long long分别调用abs、labs、llabs,float、double分别调用fabsf、fabs;按需包含stdlib.h与math.h。- 不运行,用
_Generic断定MDR_ABS(-1)、MDR_ABS(-1L)、MDR_ABS(-1LL)、MDR_ABS(-1.0f)、MDR_ABS(-1.0)的返回类型依次为int、long、long long、float、double:写一个把类型映射到类型名的辅助宏,把这五个表达式各打印一行。这五行同时验证了分支里选的是哪个函数,以及返回类型没有被提升或截断。 - 验证实参只求值一次:写一个每次调用让计数加一的
next(),执行int r = MDR_ABS(next());,确认计数为 1。再写一个普通的#define BAD_ABS(x) ((x) < 0 ? -(x) : (x)),同样以next()为实参,比较两者的计数。说明_Generic的括号里的表达式与结果表达式各被求值几次,这与宏参数展开两次的写法区别在哪里。 - 再给
short加一个分支,调用abs。说明分支里写函数名而不是写调用表达式的好处,以及为什么这个分支的返回类型是int。
验收:MDR_ABS(-3)、MDR_ABS(-4L)、MDR_ABS(-5LL)、MDR_ABS(-1.5f)、MDR_ABS(-2.5) 分别得到 3、4、5、1.50、2.50;MDR_ABS((short)-7) 得到 7。
下面的宏想按实参类型选择 printf 的转换说明,分支还没有填:
#include <stdio.h>
#define PRINT_VALUE(x) printf(_Generic((x), \
/* 补全 */ \
), (x))2
3
4
5
要求:
补全分支,使
int、unsigned int、long、long long、float、double、char *、const char *分别使用"%d\n"、"%u\n"、"%ld\n"、"%lld\n"、"%g\n"、"%g\n"、"%s\n"、"%s\n"。宏里不写default。用一组对象验证:
int取-1,unsigned int取7u,long取123456789L,long long取-9000000000LL,float取2.5f,double取3.25,字符数组buf的内容为"buffer",const char *s指向"literal pointer"。写出完整输出。增加一行
short s = 1; PRINT_VALUE(s);,编译会发生什么?为什么这里不像普通函数调用那样把short提升为int?如果把
char *分支删掉、只留const char *,PRINT_VALUE("abc")还能编译吗?说明字符串字面量在_Generic里的类型。写一个每次调用让计数加一的
next(),执行PRINT_VALUE(next()),确认计数只加了 1。宏展开后next()出现了两次:一次在_Generic的括号里,一次在printf的实参表末尾。说明为什么只有后者真正执行,计数为什么不是 2。给宏再加一个
long double: "%Lf\n"分支,但程序从不传入long double。编译能否通过?再把这一分支的字符串换成未声明的标识符undeclared_symbol,重新编译。由此说明_Generic中未被选中的分支需要满足什么条件。
strchr 的返回类型是 char *,对 const char * 实参调用时会丢掉 const。用泛型选择做一个保留 const 的版本:
char *strchr_mut(char *s, int c);
const char *strchr_const(const char *s, int c);
#define MDR_STRCHR(s, c) /* 补全 */2
3
4
(1) [3.3] 补全宏,使 char * 实参得到 char *,const char * 实参得到 const char *。两个辅助函数内部调用标准库的 strchr,strchr_const 的返回值就是 strchr 的结果,strchr_mut 需要把标准库返回的 char * 直接返回。
(2) [2.2] 写测试:对 char buf[] = "hello"; 调用 MDR_STRCHR(buf, 'l') 并改写返回的字符,输出 heLlo;对 const char *text = "hello"; 调用后把返回值交给 const char *,输出 llo。这两个结果对应 strchr 找到第一个 'l' 的位置。
(3) [3.2] 用 _Generic 检查两种调用的返回类型,分别打印 char * 与 const char *。
(4) [3.1] 写下 MDR_STRCHR(text, 'l')[0] = 'L';,编译会报什么?由此说明这个宏保住了什么。
(5) [4.2*] 只写一个返回 char * 的辅助函数 char *strchr_bad(const char *s, int c),内部直接 return strchr(s, c);。用 const char * 实参调用它,把结果赋给 char *,用 gcc -std=c17 -Wall -Wextra 编译。会得到警告吗?如果没有任何诊断,说明 const 是在哪一步丢掉的:是调用处的转换,还是 C17 里 strchr 的声明本身。再把结果赋给 const char * 并尝试通过它写入,比较两种用法的安全性。查资料说明 C23 的标准 strchr 改成类型泛型形式,解决的正是哪一种情况。