在C语言中,':-!!'是什么意思?

1809
我在/usr/include/linux/kernel.h中遇到了这段奇怪的宏代码:
/* 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); }))
:-!! 是用来做什么的?

更新:最近,宏已移至/usr/include/linux/build_bug.h


2
一元负号 <br /> ! 逻辑非 <br /> 对给定整数 e 进行反向非运算,使变量可以为 0 或 1。 - CyrillC
83
Git blame 命令告诉我们,这种特定形式的静态断言是由 Jan Beulich 在 8c87df4 中引入的(请参见提交消息)。显然他有很好的理由这样做。 - Niklas B.
2
几乎不用说,所创建的位域是匿名的。这与C++模板元编程的精神相同,即在编译时发生的事情可以在编译时进行检查。 - phorgan1
等等,我以为 sizeof 的参数不会被求值。这种情况下是不是错了?如果是,为什么?因为它是一个宏吗? - Phillip Cloud
3
@cpcloud,sizeof 确实会“评估”类型,但不涉及值。在这种情况下无效的是类型本身。 - Winston Ewert
几个答案提到 :0 给出了一个大小为零的匿名位域,因此是一个大小为零的结构体。但这并不完全正确;在标准 C 中,:0 不是任何类型的字段声明(你不能给它一个名称),而是指令以在下一个字边界(通常是 int 但不一定)上开始 下一个 位域。除非在两个(否则相邻的)位域声明之间使用,它通常没有任何意义。生成的结构体大小为零,因为它包含 没有 声明;当然这是 gcc 的扩展。 - Martin Kealey
5个回答

1806

实际上,这是一种检查表达式 e 是否可以计算为 0 的方法,如果不能,则会使构建失败。

这个宏的命名有些不准确;它应该更像是 BUILD_BUG_OR_ZERO,而不是 ...ON_ZERO。已经有偶尔讨论过这是否是一个令人困惑的名称

你应该这样阅读这个表达式:

sizeof(struct { int: -!!(e); }))
  1. (e):计算表达式 e

  2. !!(e):逻辑上两次否定:e == 0 则为 0,否则为 1

  3. -!!(e):对步骤2的表达式进行数值上的否定:如果是 0,则为 0;否则为 -1

  4. struct{int: -!!(0);} --> struct{int: 0;}:如果是零,则声明一个带有匿名整数位域且宽度为零的结构体。一切正常,我们可以继续执行。

  5. struct{int: -!!(1);} --> struct{int: -1;}:另一方面,如果不是零,那么它将是一个负数。声明任何带有负宽度的位域都会导致编译错误。

所以我们要么在结构体中得到一个宽度为0的位域,这是可以接受的,要么得到一个负宽度的位域,这将导致编译错误。然后我们取该字段的sizeof,因此我们得到一个适当宽度的size_t(如果e为零,则宽度为零)。
有些人问:为什么不使用assert呢? keithmo的答案 在此有很好的回复:
这些宏实现了编译时测试,而assert()是运行时测试。
完全正确。你不希望在运行时检测到可以在更早期发现的内核问题!这是操作系统的关键部分。只要可能,在编译时检测出问题就更好。

198
C++ 或 C 的最近版本拥有类似于 static_assert 的功能来实现相关的目的。 - Basile Starynkevitch
60
@Lundin - #error需要使用3行#if/#error/#endif代码,并且仅适用于预处理器可以访问的评估。这个技巧适用于编译器可以访问的任何评估。 - Ed Staub
261
Linux内核并不使用C++,至少在Linus仍然健在的情况下不会使用。 - Mark Ransom
10
在C语言中,布尔表达式被定义为始终评估为零或一。但实际上,产生“逻辑布尔”结果的运算符(<><=>===!=&&||)总是产生0或1。其他表达式可能会产生可用作条件的结果,但仅仅是零或非零;例如,在isdigit(c)中,其中c是数字,可以产生任何非零值(然后在条件中视为true)。 - Keith Thompson
9
关于名称的注释。它被称为...ON_ZERO,是因为它是BUG_ON的一个衍生物,BUG_ON是一个基本上是断言的宏。BUG_ON(foo) 意味着“如果在运行时foo为真,则存在错误”。相反,BUILD_BUG_ON是静态断言(在构建时检查),最后BUILD_BUG_ON_ZERO完全相同,只是整个表达式等于(size_t)0,正如问题中的注释所述。 - Xion
显示剩余18条评论

274
: 是一个位域字段。至于!!,那是逻辑双重否定,因此返回false为0,true为1。而-是减号,即算术取反。
这都只是一种诡计,目的是让编译器在无效输入时出现错误。
考虑 BUILD_BUG_ON_ZERO。当-!!(e)评估为负值时,会产生编译错误。否则,-!!(e)评估为0,并且0宽度位域的大小为0。因此,该宏将评估为具有值0的size_t
在我看来,该名称比较弱,因为实际上构建失败的情况是输入不为零的情况。 BUILD_BUG_ON_NULL非常相似,但产生指针而不是int

17
sizeof(struct { int:0; }) 是否严格符合规范? - ouah
8
为什么通常结果会是 0?一个只有空位域的结构体,是的,但我认为大小为 0 的结构体是不被允许的。例如,如果您创建了该类型的数组,则各个数组元素仍必须具有不同的地址,对吗? - Jens Gustedt
2
他们实际上并不在意使用GNU扩展,他们禁用了严格别名规则,并且不将整数溢出视为UB。但我想知道这是否是严格符合C的标准。 - ouah
3
有关未命名的零长度位域,可以参考这里:https://dev59.com/-W855IYBdhLWcg3whEpL - David Heffernan
11
@DavidHeffernan,实际上C语言允许定义宽度为0的无名位域,但是只有在结构体中存在其他命名成员时才允许这样做。(C99, 6.7.2.1p2) "如果struct-declaration-list中不包含命名成员,则行为未定义。"因此例如sizeof(struct {int a:1; int:0;})是严格符合规定的,但是sizeof(struct {int:0;})则不符合(未定义行为)。 - ouah
显示剩余6条评论

176
有些人似乎混淆了这些宏和assert()
这些宏实现了编译时测试,而assert()是运行时测试。
更新:
从C11开始,_Static_assert()关键字可用于创建编译时测试,除非为旧编译器编写代码,否则应使用它。

58

我很惊讶,这种语法的替代方案没有被提到。另一种常见(但较旧的)机制是调用未定义的函数,并依靠优化器在断言正确时编译掉函数调用。

#define MY_COMPILETIME_ASSERT(test)              \
    do {                                         \
        extern void you_did_something_bad(void); \
        if (!(test))                             \
            you_did_something_bad(void);         \
    } while (0)

虽然这种机制有效(只要启用了优化),但它的缺点是直到链接时才报告错误,此时它无法找到函数you_did_something_bad()的定义。这就是为什么内核开发人员开始使用诸如负大小位域宽度和负大小数组之类的技巧(后者在GCC 4.4中停止破坏构建)。

为了同情对编译时断言的需求,GCC 4.3引入了error函数属性,允许您扩展这个旧概念,并生成一个带有您选择的消息的编译时错误 -- 不再是晦涩的“负大小数组”错误消息!

#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中的大多数宏已相应更新。然而,此宏不能用作初始化程序。但是,使用语句表达式(另一个GCC C扩展),您可以!
#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;                                                    \
    })

这个宏将仅评估其参数一次(以防它具有副作用),并在表达式评估为五或不是编译时常量时创建一个编译时错误,该错误消息为“我告诉你不要给我五!”。
那么为什么我们不使用这个宏来代替负大小的位域呢?遗憾的是,目前对语句表达式的使用存在许多限制,包括它们作为常量初始化器的使用(对于枚举常量、位域宽度等),即使语句表达式完全是常量本身(即,在编译时可以完全评估,并且通过了__builtin_constant_p()测试)。此外,它们不能在函数体外使用。
希望GCC能很快改进这些缺陷,并允许常量语句表达式用作常量初始化器。挑战在于语言规范定义了什么是合法的常量表达式。C++11为此添加了constexpr关键字,但C11中没有相应的关键字。虽然C11确实获得了静态断言,可以解决部分问题,但它无法解决所有这些缺陷。因此,我希望gcc可以通过-std=gnuc99和-std=gnuc11等方式将constexpr功能作为扩展功能提供,并允许其在语句表达式等方面使用。

6
你的所有解决方案都不是替代方案。在宏上面的注释非常清楚:“因此,该表达式可以在结构体初始化程序(或任何其他逗号表达式不允许的地方)中使用。”该宏返回一个 size_t 类型的表达式。 - Wiz
3
是的,我知道这一点。也许我的措辞有些冗长,可能需要重新审视一下,但我的重点是探讨静态断言的各种机制,并展示为什么我们仍然在使用负大小的位域。简而言之,如果我们有了常量表达式语句的机制,我们将有其他选项可供选择。 - Daniel Santos
无论如何,我们不能将这些宏用于变量。对吧?error: bit-field ‘<anonymous>’ width not an integer constant 它只允许使用常量。那么,有什么用呢? - Karthik Raj Palanichamy
1
@Karthik,请搜索Linux内核的源代码,以了解其用途。 - Daniel Santos

39

如果条件为假,它将创建大小为0的位域,但如果条件为真/非零,则创建大小为-1-!!1)的位域。在前一种情况下,没有错误,并且结构体被初始化为一个int成员。在后一种情况下,会出现编译错误(当然也不会创建大小为-1的位域)。


3
如果条件成立,它实际上会返回一个值为0的 size_t - David Heffernan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接