[英]Linux Kernel's __is_constexpr Macro
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
宏 __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 <Martin.Uecker@med.uni-goettingen.de>
*/
#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)
都不是null指针常量 , 无论其值如何 。
知道这一点,我们可以看到随后发生的情况:
8 ? ((void *)((long)(x) * 0l)) : (int *)8
注意:第二个8
字面量旨在避免编译器警告有关创建指向未对齐地址的指针。 前8
文字可以简单地为1
。 但是,这些细节对于理解宏的关键点并不重要。
这里的关键是条件运算符根据操作数之一是否为空指针常量 (6.5.15.6)返回不同的类型 :
[...]如果一个操作数是一个空指针常量,则结果具有另一种操作数的类型; 否则,一个操作数是一个指向空隙或一个合格的版本的空隙 ,在这种情况下,结果类型是指向的空隙的适当资格版本。
因此,如果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)的过程中。
为了方便起见,希望在-Wvla
内核范围内启用GCC的-Wvla
警告 ; 因此,VLA的所有实例都由编译器标记。
启用警告后,事实证明,GCC报告了许多阵列为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标准,由于使用了逗号运算符 (6.6.3),所以((void)0, 100)
0,100 ((void)0, 100)
不是 常数表达式 (6.6)。 在这种情况下, 即使 GCC 知道大小是一个编译时常量,它也会选择发布VLA代码。 相比之下,没有。
由于内核中的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代码。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.