Linux内核的__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 <Martin.Uecker@med.uni-goettingen.de>
*/
#define __is_constexpr(x) \
(sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))
有关解决同一问题的不同方法的讨论,请参阅:检测宏中的整数常量表达式
__is_constexpr(x)
宏可以在Linux内核的include/core/core. 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 <Martin.Uecker@med.uni-goettingen.de>
*/
#define __is_constexpr(x) \
(sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))
它是在2018-04-05Linux内核v4.17,提交3c8ba0d61d04的合并窗口中引入的;尽管围绕它的讨论在一个月前就开始了。
宏以利用C标准的微妙细节而闻名:条件运算符确定其返回类型的规则(6.5.15.6)和空指针常量的定义(6.3.2.33)。
此外,它依赖于允许sizeof(void)
(与sizeof(int)
不同),它是一个GNU C扩展。
宏的主体是:
(sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))
让我们专注于这一部分:
((void *)((long)(x) * 0l))
注意:(long)(x)
强制转换旨在允许x
具有指针类型,并避免32位平台上u64
类型的警告。但是,这个细节对于理解宏的关键点并不重要。
如果x
是一个整数常量表达式(6.6.6),那么((long)(x)*0l)
是value0
的整数常量表达式。因此,(void*)((long)(x)*0l)
是一个空指针常量(6.3.2.33):
值为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)
宏返回value1
的整数常量表达式。否则,它返回value0
的整数常量表达式。
宏是在从Linux内核中删除所有可变长度数组(VLA)的过程中出现的。
为了方便这样做,最好在内核范围内启用GCC的-Wvla
警告;以便编译器标记所有VLA实例。
启用警告后,结果发现GCC报告了许多数组是VLA的情况,而这并非旨在如此。例如,在fs/btrfs/tree-checkker. 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)
不是常量表达式(6.6),因为使用了逗号运算符(6.6.3)。在这种情况下,GCC选择发出VLA代码,即使它知道大小是编译时常量。相反,Clang没有。
由于内核中的max(x, y)
宏不是常量表达式,GCC触发了警告并生成了内核开发人员不打算使用的VLA代码。
因此,一些内核开发人员试图开发替代版本的max
和其他宏来避免警告和VLA代码。一些尝试试图利用GCC的__builtin_constant_p
内置,但是没有方法适用于当时内核支持的所有版本的GCC(gcc
在某个时候,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代码。