1 概述
在linux内核中,typeof()关键字用到的地方很多,如之前写了一篇文章ring buffer,一篇文章讲透它?。里面所介绍的linux(5.17+)版本kfifo.c和kfifo.h中,在实现ring buffer的函数接口时,就经常看到typeof()的使用,如下kfifo_is_empty(fifo)、kfifo_len(fifo)、kfifo_reset(fifo)三个接口的宏定义实现,均使用了关键字typeof()。以这个例子为引子,本篇文章将会系统的介绍关键字typeof()的用法。
#define kfifo_is_empty(fifo) \({ \typeof((fifo) + 1) __tmpq = (fifo); \__tmpq->kfifo.in == __tmpq->kfifo.out; \})#define kfifo_len(fifo) \({ \typeof((fifo) + 1) __tmpl = (fifo); \__tmpl->kfifo.in - __tmpl->kfifo.out; \})#define kfifo_reset(fifo) \(void)({ \typeof((fifo) + 1) __tmp = (fifo); \__tmp->kfifo.in = __tmp->kfifo.out = 0; \})
2 含义
首先明确,typeof并不是标准 c定义(C89/90)的关键字,而是 GUN C 提供的一种特性 (参考:C-Extensions))。C++ 中的typeof是由 C++ 标准库提供,定义于<typeinfo>头文件中,C++11 标准新增的decltype是typeof的升级版本。在开发或者进行代码移植时一定要注意编译器支持的c/c++版本,可能存在typeof无法使用的情况。
typeof是编程语言的一种运算符,用于确定变量的数据类型。这在程序结构可以接受多种数据类型且无需显式指出具体类型时非常有用。—— 维基百科
从上面维基百科中对typeof关键字的解释可以看出,其主要作用是确定所传入参数的数据类型。由于c/c++是编译型语言,在代码的编写过程中,对于全局变量、局部变量、函数参数、函数返回值等类型肯定是在定义时就已经确定了(或者说是已经知道了)。因此,在一般的代码编写中,typeof关键字是用不到的。但是在宏定义中,尤其在宏定义函数中,其所传入的参数类型是未知的,或者是不明确的,这样可能在编译的时候没有问题,但在运行时就会出错(dump)。这种情况,如果在宏定义函数时,合理使用typeof关键字,可以在编译阶段就把问题暴露出来。
关键字typeof()能够接受两种类型的参数:
- 表达式;
- 具体的数据类型。
1. 表达式:
typeof (x) y;,含义是定义了一个和x类型一样的变量y;typeof (*x) y;,含义是定义了一个指向x数据类型的指针变量y;typeof (typeof (char *)[4]) y;,含义是定义了一个长度为 4 的指针数组,数组元素是char *类型的指针,和char *y[4];是等价的;
2. 具体的数据类型:
typeof (int *) y;,含义是定义了一个int类型的指针y,和int * y;等价;typeof (int) y;,含义是定义了一个int类型的整形变量y,和int y等价。
下面来看一下typeof关键字的典型使用场景。
3 典型使用场景
3.1 在宏定义函数中获取传入参数的数据类型
如下定义了两个宏MAX1和MAX2,用来比较出两个元素中的较大者。MAX2很简单,一般的比较大小的宏函数可能也是这么定义的。重点看一下MAX1,其定义中使用typeof重新定义了两个局部变量_max1和_max2。精华的语句就是(void) (&_max1 == &_max2);,将两个局部变量的指针(地址)进行比较,如果两个变量的类型不一样,上面这条语句在编译阶段会给出一个警告(comparison of distinct pointer types lacks a cast)。MAX2则不会有任何警告。
下面这段程序其实输出的是一个错误的结果,a=1、b=1.1,本来两者较大者应该是1.1,最后输出的却是1。MAX1在编译阶段起码会给出一个警告,MAX2则不会有任何提示信息。MAX1要优于MAX2。
#include <stdio.h>#define MAX1(x, y) ({ \typeof(x) _max1 = (x); \typeof(y) _max2 = (y); \(void) (&_max1 == &_max2); \_max1 > _max2 ? _max1 : _max2; })#define MAX2(a,b) ((a>b)? a:b)int main(int argc, char *argv[]){int a = 1;float b = 1.1;int c = 0;c = MAX1(a,b);printf("max0:%d\n", c);c = MAX2(a,b);printf("max1:%d\n", c);return 0;}
运行结果输出:
max0: c = 1max1: c = 1
3.2 在宏定义函数中对入参进行参数检查
如下是linux(5.17+)版本kfifo.c和kfifo.h中ring buffer的判空实现函数。在kfifo_is_empty(fifo)实现中,有一个typeof((fifo) + 1)操作,其要求传入的fifo必须是一个指针(结构体指针),因为指针做加一操作是正常的,而结构体 (fifo 是结构体指针) 加一操作在编译阶段就会报错。指针加一后的结果和指针本身的类型是相同的,也就是说typeof((fifo) + 1)的类型还是和fifo的数据类型相同。也就是说使用typeof((fifo) + 1)而不是直接使用typeof(fifo),可以多一层参数检查,及早暴露出问题来,在linux内核中很多地方都用了这个技巧。
#define kfifo_is_empty(fifo) \({ \typeof((fifo) + 1) __tmpq = (fifo); \__tmpq->kfifo.in == __tmpq->kfifo.out; \})
3.3 不用知道函数返回值类型,可以用 typeof() 定义一个用于接收该函数返回值的变量
可以看到如下main函数中,变量v的定义语句:typeof(get_version_info()) v;。有了typeof关键字,在不用知道get_version_info函数返回值类型,也可以定义该函数返回值的接收变量。注意语句typeof(get_version_info()) v;并不会引起函数get_version_info()的执行,其只是取出了该函数的返回值类型。
#include <stdio.h>#include <stdlib.h>#include <string.h>struct verson_info_t{char version[20];char sub_version[20];char date[20];};struct verson_info_t *get_version_info(){struct verson_info_t *verson_info;verson_info = malloc(sizeof(struct verson_info_t));if(verson_info == NULL){printf("malloc error.\n");return verson_info;}strncpy(verson_info->version,"v2",strlen("v2"));strncpy(verson_info->sub_version,"3.2",strlen("3.2"));strncpy(verson_info->date,"y22w50.4",strlen("y22w50.4"));return verson_info;}int main(int argc, char *argv[]){typeof(get_version_info()) v;v = get_version_info();printf("version:%s\n", v->version);printf("sub_version:%s\n", v->sub_version);printf("date:%s\n", v->date);return 0;}
运行结果输出:
version:v2sub_version:3.2date:y22w50.4
4 注意
typeof关键字中的类型名不能包含存储类说明符,如extern或static。不过允许包含类型限定符,如const或volatile。如下面代码是无效的,因为它在typeof()中包含了extern:
typeof(extern int) a;
5 总结
- typeof是编程语言的一种运算符,用于确定变量的数据类型;
- 关键字typeof能够接受两种类型的参数:表达式和具体的数据类型;
- 在开发或者进行代码移植时一定要注意编译器支持的
c/c++版本,可能存在typeof无法使用的情况; - typeof可以在宏定义函数中获取传入参数的数据类型;
- typeof可以在宏定义函数中对入参进行参数检查;
- 不用知道函数返回值类型,可以用typeof定义一个用于接收该函数返回值的变量。
https://juejin.cn/post/7115796606783324173
