Linux内核的__is_constexpr宏
Linux Kernel的__is_constexpr(x)
宏如何工作? 它的目的是什么? 什么时候介绍? 为什么要介绍?
/* * This returns a constant expression while determining if an argument is * a constant expression, most importantly without evaluating the argument. * Glory to Martin Uecker */ #define __is_constexpr(x) \ (sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))
有关解决相同问题的不同方法的讨论 ,请参阅: 检测宏中的整数常量表达式
Linux内核的__is_constexpr
宏
介绍
__is_constexpr(x)
宏可以在Linux内核的include / kernel / kernel.h中找到 :
/* * This returns a constant expression while determining if an argument is * a constant expression, most importantly without evaluating the argument. * Glory to Martin Uecker */ #define __is_constexpr(x) \ (sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))
它是在Linux Kernel v4.17的合并窗口中引入的,在2018-04-05 提交3c8ba0d61d04 ; 虽然围绕它的讨论开始于一个月前。
该宏值得注意的是利用C标准的细微细节: 条件运算符确定其返回类型的规则(6.5.15.6)和空指针常量的定义(6.3.2.3.3)。
此外,它依赖于允许的sizeof(void)
(并且与sizeof(int)
),这是GNU C扩展 。
它是如何工作的?
宏的身体是:
(sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))
让我们关注这一部分:
((void *)((long)(x) * 0l))
注意: (long)(x)
u64
旨在允许x
具有指针类型并避免在32位平台上对u64
类型发出警告。 但是,这个细节对于理解宏的关键点并不重要。
如果x
是 整数常量表达式 (6.6.6),那么((long)(x) * 0l)
是值0
的整数常量表达式 。 因此, (void *)((long)(x) * 0l)
是空指针常量 (6.3.2.3.3):
值为0的整型常量表达式或类型为void *的表达式称为空指针常量
如果x
不是 整数常量表达式 ,则(void *)((long)(x) * 0l)
不是空指针常量 , 无论其值如何 。
知道了,我们可以看到之后会发生什么:
8 ? ((void *)((long)(x) * 0l)) : (int *)8
注意:第二个8
字面旨在避免编译器警告有关创建指向未对齐地址的指针。 前8
文字可能只是1
。 但是,这些细节对于理解宏的关键点并不重要。
这里的关键是条件运算符返回一个不同的类型,具体取决于其中一个操作数是否为空指针常量 (6.5.15.6):
[…]如果一个操作数是空指针常量,则结果具有另一个操作数的类型; 否则,一个操作数是指向void的指针或void的限定版本,在这种情况下,结果类型是指向适当限定版本的void的指针。
因此,如果x
是一个整型常量表达式 ,那么第二个操作数是空指针常量 ,因此表达式的类型是第三个操作数的类型,它是指向int
的指针。
否则 ,第二个操作数是指向void
的指针,因此表达式的类型是指向void
的指针。
因此,我们最终有两种可能性:
sizeof(int) == sizeof(*((int *) (NULL))) // if `x` was an integer constant expression sizeof(int) == sizeof(*((void *)(....))) // otherwise
根据GNU C扩展 , sizeof(void) == 1
。 因此,如果x
是整数常量表达式 ,则宏的结果为1
; 否则, 0
。
此外,由于我们只是比较两个sizeof
表达式的相等性,结果本身是另一个整数常量表达式 (6.6.3,6.6.6):
常量表达式不应包含赋值,递增,递减,函数调用或逗号运算符,除非它们包含在未评估的子表达式中。
整数常量表达式应具有整数类型,并且只能具有整数常量的操作数,枚举常量,字符常量,结果为整数常量的sizeof表达式,以及作为强制转换的直接操作数的浮点常量。 整数常量表达式中的转换运算符只能将算术类型转换为整数类型,除非作为sizeof运算符的操作数的一部分。
因此,总之,如果参数是整数常量表达式, __is_constexpr(x)
宏将返回值为1
的整数常量表达式。 否则,它返回值为0
的整数常量表达式 。
为什么要介绍?
在从Linux内核中删除所有可变长度数组(VLA)的 过程中,宏出现了。
为了促进这一点,需要在内核范围内启用GCC的-Wvla
警告 ; 以便编译器标记所有VLA实例。
当启用警告时,结果发现GCC报告了许多arrays为VLA的情况,而这些情况并非如此。 例如在fs / btrfs / tree-checker.c中 :
#define BTRFS_NAME_LEN 255 #define XATTR_NAME_MAX 255 char namebuf[max(BTRFS_NAME_LEN, XATTR_NAME_MAX)];
开发人员可能希望max(BTRFS_NAME_LEN, XATTR_NAME_MAX)
已解析为255
,因此应将其视为标准数组(即非VLA)。 但是,这取决于max(x, y)
宏扩展到什么。
关键问题是如果数组的大小不是C标准定义的(整数)常量表达式 ,GCC会生成VLA代码。 例如:
#define not_really_constexpr ((void)0, 100) int a[not_really_constexpr];
根据C90标准, ((void)0, 100)
0,100 ((void)0, 100)
不是 常数表达式 (6.6),因为使用了逗号运算符 (6.6.3)。 在这种情况下,GCC选择发出VLA代码, 即使它知道大小是编译时常量 。 相比之下,Clang没有。
由于内核中的max(x, y)
宏不是常量表达式,因此GCC触发了警告并生成了内核开发人员不想要的VLA代码。
因此,一些内核开发人员试图开发max
和其他宏的替代版本以避免警告和VLA代码。 一些尝试试图利用GCC的__builtin_constant_p
内置 ,但没有一种方法适用于当时内核支持的所有GCC版本( gcc >= 4.4
)。
在某些时候,Martin Uecker 提出了一种特别聪明的方法,它不使用内置函数 (从glibc的tgmath.h中 获取灵感 ):
#define ICE_P(x) (sizeof(int) == sizeof(*(1 ? ((void*)((x) * 0l)) : (int*)1)))
虽然该方法使用GCC扩展, 但它仍然很受欢迎 ,并被用作__is_constexpr(x)
宏背后的关键思想,它在与其他开发人员进行几次迭代后出现在内核中。 然后使用该宏来实现max
宏和其他需要为常量表达式的宏,以避免GCC生成VLA代码。