我在/usr/include/linux/kernel.h中碰到了这个奇怪的宏代码:
1 2 3 4 5 6
| /* Force a compilation error if condition is true, but also produce a
result (of value 0 and type size_t), so the expression can be used
e.g. in a structure initializer (or where-ever else comma expressions
aren't permitted). */
#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!!(e); }))
#define BUILD_BUG_ON_NULL(e) ((void *)sizeof(struct { int:-!!(e); })) |
:-!!是做什么的?
- -一元减号
!logical not
inverse not of the given integer e so the variable can be 0 or 1.
- @lundin:git-beulich在8c87df4中介绍了这种特殊的静态断言形式。显然,他有充分的理由这样做(参见提交消息)。
- 尼克拉斯布。他改变的宏观世界同样模糊…#define BUILD_BUG_ON_ZERO(e) (sizeof(char[1 - 2 * !!(e)]) - 1)。
- @Lundin:如果这是远离Linux机器的原因,那么请只命名一个源代码看起来更好的现代操作系统。
- @顺便说一下,svenmarnach bsd内核(:p)。我并不认为BSD内核更好,但它们常常为了干净的代码而牺牲性能(和黑客行为)。哦,是的,我一直在处理BSD和Linux内核代码。
- @伦丁:对你来说,疯牛病看起来不那么模糊吗?任何人都可以为Linux做出贡献,那为什么不试一下,而不是远离现代操作系统呢?
- @lundin:assert()不会导致编译时错误。这就是上述结构的全部要点。
- sizeof变体是我在许多不同公司的多个项目中见过/使用的C代码变体。我不会说它晦涩难懂。我同意这个新的方法是模糊的,但是基于提交消息中的推理,这是一个肯定的改进,我希望在几年内我能看到它无处不在。(假设我们没有看到C11静态断言的广泛实现。)
- @Grewekokkor并不幼稚,Linux对于一个人来说太大了,无法处理所有的问题。利纳斯有他的六个租户,他们从下到上推动变革和改进。利纳斯只决定他是否想要这个角色,但他在某种程度上信任同事。如果您想了解更多关于分布式系统如何在开源环境中工作的信息,请查看youtube视频:youtube.com/watch?V=4ppnkhjaok8(这是一个非常有趣的话题)。
- 几乎不用说创建的位域是匿名的。这与C++模板元编程的精神相同,即编译时发生的事情可以在编译时检查。
- 等等,我以为江户十一〔一〕的论据没有被评估。在这种情况下是不是错了?如果是,为什么?因为它是一个宏?
- @cpcloud,sizeof不"评估"类型,只是不评估值。它的类型在本例中无效。
- 以东十一〔四〕是笑脸,你没看见吗?:)
实际上,这是一种检查表达式e是否可以计算为0的方法,如果不能计算为0,则会使生成失败。
宏的名称有点错误;它应该更像BUILD_BUG_OR_ZERO,而不是...ON_ZERO。(关于这是否是一个令人困惑的名字,偶尔会有讨论。)
您应该这样阅读表达式:
1
| sizeof(struct { int: -!!(e); })) |
(e):计算表达式e。
!!(e):逻辑上否定两次:e == 0时为0;否则为1。
-!!(e):用数字否定步骤2的表达式:如果是0,则为0;否则为-1。
struct{int: -!!(0);} --> struct{int: 0;}:如果它是零,那么我们声明一个具有宽度为零的匿名整数位域的结构。一切都很好,我们照常进行。
struct{int: -!!(1);} --> struct{int: -1;}:另一方面,如果不是零,那么它将是一个负数。声明宽度为负的任何位域都是编译错误。
因此,我们要么得到一个结构中宽度为0的位字段(很好),要么得到一个宽度为负的位字段(编译错误)。然后我们取sizeof这个字段,得到一个宽度合适的size_t(在e为零的情况下,宽度为零)。
有人问:为什么不直接使用assert?
Keithmo的回答很好:
These macros implement a compile-time test, while assert() is a run-time test.
完全正确。您不希望在运行时检测内核中可能在早期被捕获的问题!它是操作系统的关键部分。不管在编译时能在多大程度上检测到问题,都会更好。
- 那么,这个宏是如何使用的,也就是说,e的值或表达式是什么?
- @韦斯顿有很多不同的地方。你自己看看!
- 另外,使用错误有什么问题?
- 最近的C++或C标准变体具有类似于EDCOX1(10)的用途。
- @巴西尔:使用C++的最近变体,你应该使用模板元编程:
- @lundin-error需要使用3行代码,if/error/endif,并且只适用于预处理程序可访问的评估。此黑客程序适用于编译器可访问的任何评估。
- Linux内核不使用C++,至少在莱纳斯还活着的时候没有。
- 这就是为什么在D中引入了静态断言的原因。
- 回复:static_assert,Linux构建的所有地方都没有这个功能。
- 值得注意的是,!!e的计算结果不是零或非零正数,而是零或一。C中的布尔表达式被定义为始终计算为零或一。
- @DOLDA2000:+1.同意,这也许措词不当。我只是在这里指出,只要它是一个非零的正数,它解析为什么特定的数字就无关紧要了。您可以在步骤5中看到,我使用了1的确切值。
- 我也很想知道,特别是当我明确地说0或1的时候,但我假设你写得像一个纯数学家,并且声称仅仅足够证明这一点,而且仅仅是必要的。
- "您不想在运行时检测到任何可能在早期被发现的问题!"帮你修好了。
- 此测试的另一个重要点是,是否可以在编译时对表达式进行计算,如果不能,则生成将失败。
- @Dolda2000:虽然我认为你的评论是正确的(最终结果是0或1),你能详细说明它是如何获得的吗?附加问题:为什么,如果e是负数,!!(e)是正面的吗?是!操作员要求进行类型转换?谢谢您。
- @dolda2000:"C中的布尔表达式被定义为始终计算为零或一"——不完全正确。产生"逻辑布尔"结果的运算符(!、<、>、<=、>=、==、!=、&&、||总是产生0或1。其他表达式可能产生可用作条件的结果,但仅为零或非零;例如,isdigit(c),其中c是数字,可以产生任何非零值(然后在条件中视为真)。
- 因为什么时候允许sizeof任何类型为零?这违反了数组元素地址的唯一性。(这是C,所以我们不要进入空的基类优化,它仍然不会导致任何对象的大小为零)
- @Benvoigt C标准不允许零大小类型。Linux内核依赖于许多不属于标准的gcc编译器扩展。如:stackoverflow.com/questions/8143417/&hellip;
- @约翰:我知道内核使用GCC扩展。但是扩展仅仅给无效代码赋予了意义。如果编译器改变了有效代码的含义,那就是错误。
- 我做C编程已经有一段时间了,还没有看到这个"位字段"功能。所以对于同一职位的人,这里有一个很好的教程:tutorialspoint.com/cprogramming/c_bit_fields.htm
- 关于名字的简短说明。它被称为...ON_ZERO,因为它是BUG_ON的衍生物,一个本质上是断言的宏。BUG_ON(foo)的意思是"如果foo是真的(在运行时),这是一个bug"。相反,BUILD_BUG_ON是一个静态断言(在构建时进行检查),最后BUILD_BUG_ON_ZERO完全相同,只是整个内容是一个与(size_t)0相等的表达式,正如问题中的注释所述。
- @谢:谢谢你的解释。但我得承认,我觉得BUILD_BUG_ON_ZERO这个名字非常令人困惑,希望我不需要搜索互联网来了解它是如何使用的。我不知道什么名字对每个人都有用,但就我而言,我希望使用的是BUILD_BUG_OR_ZERO这个名字。
- 无论如何,我们不能对变量使用这些宏。正确的?error: bit-field ‘’ width not an integer constant它只允许常量。那么,有什么用?
- 为什么要做双重否定,因为双重否定的最终结果和原值相同。
- @大餐你和~混淆了。!只产生0或1。
- 我认为答案应该提到,根据C标准,struct { int :0}是undefined behaviour。stackoverflow.com/questions/4297095/&hellip;
- 最后一个括号中还有一个附加的右括号,它属于宏,以防有人想知道为什么要有一个附加的右括号。我试图编辑,但编辑至少需要6个字符。
- @hejazzman-re:静态断言,您可以像这样使用静态断言,对吗?#if (__STDC_VERSION__ >= 201100L) #include #else #define static_assert(e, txt) BUILD_BUG_ON_ZERO(e) #endif
:是一个位字段。对于!!,这是逻辑双否定,因此返回0表示假,返回1表示真。-是一个负号,即算术否定。
这只是让编译器在无效输入上死记硬背的一个技巧。
以BUILD_BUG_ON_ZERO为例。当-!!(e)计算为负值时,会产生编译错误。否则,-!!(e)的计算结果为0,0宽度的位字段的大小为0。因此,宏计算为值为0的size_t。
在我的视图中,这个名称很弱,因为当输入不是零时,构建实际上会失败。
BUILD_BUG_ON_NULL非常相似,但产生一个指针而不是int。
- sizeof(struct { int:0; })是否严格符合?
- 为什么结果通常是0?只有空位字段的struct,true,但我认为不允许使用大小为0的结构。例如,如果您要创建一个该类型的数组,那么各个数组元素仍然必须有不同的地址,不是吗?
- 它们实际上并不关心使用GNU扩展,它们禁用严格的别名规则,也不将整数溢出视为UB。但我想知道这是否严格符合C。
- @关于未命名的零长度位字段,请参见:stackoverflow.com/questions/4297095/&hellip;
- @ DavidHeffernan有趣,但引用的是C++标准而不是C
- 大小0位字段用于强制对齐。
- @在C标准中也有几乎相同的文本。
- @davidheffernan实际上c允许0宽度的未命名位字段,但如果结构中没有其他命名成员,则不允许。(C99, 6.7.2.1p2)"If the struct-declaration-list contains no named members, the behavior is undefined.",例如sizeof (struct {int a:1; int:0;})是严格符合的,但sizeof(struct { int:0; })不是(未定义的行为)。
- @戴维德:你能,求你了,去证明一下,为什么结果是!!(e)是0还是1(没有其他非零数字)?我想念那部分。原始E中的其他两次求反的位会发生什么情况?它们不检索原始值?谢谢您。
- @axeoth !是逻辑否定,"~"是位否定。所以我们在这里讨论逻辑否定。逻辑求反运算符的计算结果为0或1。
- @Jensgustedt:对于零大小的对象,坚持与非零大小的对象相同的保证是不实际的,但对于真正的零也一样。如果p和q是指向零大小类型的指针,则从p中加上或减去任何值都将产生p,如果p==q,p-q将产生0,在所有其他情况下都将产生未指定的值。我能看到的唯一缺点是它会呈现无用的代码,这些代码试图使用大小为零的数组(而不是-1)在所需条件不成立时强制编译器出错。
有些人似乎把这些宏与assert()混淆了。
这些宏实现编译时测试,而assert()是运行时测试。
- 那又怎么样?Linux开发人员是在编译期间哭,还是在第一次执行期间哭,这有什么关系?只要生产代码做了它应该做的。
- @你在开玩笑吗?显然,在你的内核中有一个编译器错误要比在你发射一枚巡航导弹之前不被发现要好得多,因为它的制导系统会引发内核恐慌。
- @约翰菲米内拉:是的,我在开玩笑,谁听说过有人真的试图运行他们写的代码!测试-哈!它会编译,发货!
- @约翰布沙南,你是说多斯?;)
- @伦丁:如果你仍然认为这无关紧要,那不是讽刺和讽刺。这很重要。它非常重要,编译错误比运行时错误早了几英里。如果您认为修复运行时错误所需要的就是至少运行一次代码…请看我上面的评论。
- @gparent您显然不了解这个特定问题的本质,这就是讨论的内容,而不是一般运行时错误。这个特定场景的assert()将失败100%的执行,直到错误得到纠正。如果愿意的话,这是一个静态签入运行时。
- @伦丁:事实上,我确实理解这一切,你只是没有让它明显你做。不管怎样,它在编译时还是比在运行时好,这使得整个论点都无关紧要。
- @伦丁:只有在执行assert时。意识到我的测试至少错过了一个代码路径并不罕见。
- 指出Linux内核肯定不会在第一次甚至第n次运行时执行每个代码路径。使用运行时断言与静态断言运行较少使用的代码路径需要一组罕见的参数,正如@johnfeminella的例子所示,您刚刚炸毁了中国。如果我们能解决图灵的停顿问题,那么这就不是一个问题(理论上)。
- @伦丁:是的,奇克斯上面说的。编译时和运行时断言之间的区别是白天和黑夜。您无法保证某些代码路径会被命中,有时在运行某些代码路径之前,可能需要数千次迭代和变量(或更多)。现在,我主要使用assert_early()宏来生成一个构建时断言(如果可能的话),如果不生成运行时断言的代码。
- 即使假设@lundin是正确的,一个特定的assert会沿着一个公共的代码路径被检查;这也是另一个原因,因为您不希望在内核上运行多余的代码,也不需要在内核上运行多余的代码,因为这样会进一步影响到内核的性能。这不像是写一个桌面应用程序或一次性的程序——Linux内核就像一个紧密的循环,即使在嵌入式硬件上也需要执行。
- 无论如何,我们不能对变量使用这些宏。正确的?error: bit-field ‘’ width not an integer constant它只允许常量。那么,有什么用?
嗯,我很惊讶没有提到这种语法的替代品。另一种常见的(但较旧的)机制是调用一个未定义的函数,如果断言正确,则依赖优化器编译出函数调用。
1 2 3 4 5 6
| #define MY_COMPILETIME_ASSERT(test) \
do { \
extern void you_did_something_bad(void); \
if (!(test)) \
you_did_something_bad(void); \
} while (0) |
当这个机制工作时(只要启用了优化),它的缺点是在链接之前不报告错误,这时它找不到您所做的函数的定义。这就是为什么内核开发人员开始使用一些技巧,比如负大小的位字段宽度和负大小的数组(后者停止打破GCC4.4中的构建)。
为了满足编译时断言的需要,GCC4.3引入了error函数属性,允许您扩展这个旧概念,但通过您选择的消息生成编译时错误—不再是神秘的"负大小数组"错误消息!
1 2 3 4 5 6 7
| #define MAKE_SURE_THIS_IS_FIVE(number) \
do { \
extern void this_isnt_five(void) __attribute__((error( \
"I asked for five and you gave me" #number))); \
if ((number) != 5) \
this_isnt_five(); \
} while (0) |
事实上,从Linux 3.9开始,我们现在有了一个名为compiletime_assert的宏,它使用了这个特性,并且bug.h中的大多数宏都已经相应地更新了。但是,此宏不能用作初始值设定项。但是,使用BY语句表达式(另一个GCC C扩展),您可以!
1 2 3 4 5 6 7 8 9
| #define ANY_NUMBER_BUT_FIVE(number) \
({ \
typeof(number) n = (number); \
extern void this_number_is_five(void) __attribute__(( \
error("I told you not to give me a five!"))); \
if (n == 5) \
this_number_is_five(); \
n; \
}) |
这个宏将只计算一次它的参数(以防有副作用),并创建一个编译时错误,上面写着"我告诉过你不要给我五个!"如果表达式的计算结果为5或不是编译时常量。
那么,为什么不使用这个字段来代替负大小的位字段呢?唉,目前使用语句表达式有许多限制,包括将其用作常量初始值设定项(用于枚举常量、位字段宽度等),即使语句表达式本身是完全恒定的(即,可以在编译时完全计算,否则通过__builtin_constant_p()测试)。此外,它们不能在函数体之外使用。
希望GCC能尽快修正这些缺点,并允许常量语句表达式用作常量初始值设定项。这里的挑战是定义什么是法律常量表达式的语言规范。C++ 11为这个类型或事物添加了CONSTEPRPR关键字,但是在C11中没有对应的存在。虽然C11确实得到了静态断言,这将解决部分问题,但它不能解决所有这些缺点。因此,我希望GCC可以通过-std=gnuc99&;-std=gnuc11或其他类似的方式使constexr功能作为扩展,并允许在语句表达式等上使用它。
- 您的所有解决方案都不是备选方案。宏上面的注释非常清楚"so the expression can be used e.g. in a structure initializer (or where-ever else comma expressions aren't permitted).",宏返回一个size_t类型的表达式。
- @是的,我知道。也许这有点冗长,也许我需要重新访问我的措辞,但我的观点是探索静态断言的各种机制,并说明为什么我们仍然使用负大小的位字段。简而言之,如果我们得到一个常量语句表达式的机制,我们将打开其他选项。
- 无论如何,我们不能对变量使用这些宏。正确的?error: bit-field ‘’ width not an integer constant它只允许常量。那么,有什么用?
- @Karthik搜索Linux内核的源代码,以了解使用它的原因。
- @Supercat我看不出你的评论有什么关联。你能不能修改一下,最好解释一下你的意思,或者删掉它?
如果条件为假,则创建一个大小为0的位字段;如果条件为真/非零,则创建一个大小为-1(-!!1的位字段。在前一种情况下,没有错误,结构是用int成员初始化的。在后一种情况下,会出现编译错误(当然,不会创建大小为-1的位字段)。
- 实际上,它返回值为0的size_t,以防条件为真。