什么是 ”:-!!” 用C代码?


1664

我碰到了/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); }))

怎么:-!!办?


2
-一元减<br />!逻辑NOT <br />逆不不是给定整数e,以便可变可以是0或1
CyrillC

69
git blame告诉我们,这种特定形式的静态断言是Jan Beulich在8c87df4中引入的。显然,他有充分的理由这样做(请参阅提交消息)。
Niklas B.

55
@Lundin:assert()不会导致编译时错误。这就是以上构造的重点。
克里斯·帕切霍

4
@GreweKokkor不要天真,Linux太大了,一个人都无法处理所有问题。莱纳斯(Linus)有他的狂热分子,他们有自己的人从下至上推动变革和改进。Linus只决定他是否想要功能,但他在一定程度上信任同事。如果您想进一步了解分布式系统在开源环境中的工作方式,请查看youtube视频:youtube.com/watch? v=4XpnKHJAok8(这是一个非常有趣的演讲)。
Tomas Pruzina

3
@cpcloud,sizeof“评估”类型,而不是“评估”值。在这种情况下,其类型无效。
温斯顿·埃韦特2012年

Answers:


1691

实际上,这是一种检查表达式e是否可以评估为0的方法,如果不能,则使build失败

该宏的名称有些错误;它应该更像是BUILD_BUG_OR_ZERO,而不是...ON_ZERO。(偶尔会讨论这个名称是否令人困惑。)

您应该这样阅读表达式:

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

  2. !!(e):逻辑上取反两次:0if e == 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具有适当宽度的a(如果e为零,则为零)。


有人问:为什么不只使用assert

keithmo的回答在这里得到了很好的回应:

这些宏实现编译时测试,而assert()是运行时测试。

非常正确。您不想在运行时检测内核中可能早已发现的问题!这是操作系统的关键部分。无论在何种程度上可以在编译时检测到问题,都更好。


5
@weston很多地方。你自己看!
John Feminella 2012年

166
C ++或C标准的最新变体具有类似static_assert的用途。
Basile Starynkevitch 2012年

54
@Lundin-#error将需要使用3行代码#if /#error /#endif,并且仅适用于预处理器可访问的评估。该技巧适用于编译器可访问的任何评估。
Ed Staub,2012年

236
Linux内核不使用C ++,至少在Linus还在运行时不使用。
Mark Ransom'2

6
@ Dolda2000:“ C中的布尔表达式已定义为始终求值为零或一 ” –不完全是。的运营商的是产量“逻辑的布尔”结果(!<><=>===!=&&||)总是产生0或1的其他表达式可以产生可以用作条件的结果,但仅是零或非零; 例如,,isdigit(c)其中c是一个数字,可以产生任何非零值(然后在条件中被视为true)。
基思·汤普森

256

:是位域。至于!!,这是逻辑双重否定,因此返回0false或1true。并且-是减号,即算术求反。

这只是使编译器对无效输入bar之以鼻的一种技巧。

考虑一下BUILD_BUG_ON_ZERO。当-!!(e)评估为负值时,将产生编译错误。否则-!!(e)求值为0,并且宽度为0的位域的大小为0。因此,宏的求值为a size_t,值为0。

在我看来,这个名称很薄弱,因为当输入为零时,构建实际上会失败。

BUILD_BUG_ON_NULL与非常相似,但产生的是指针而不是int


14
sizeof(struct { int:0; })严格符合?
哇,2012年

7
为什么结果一般如此0struct仅有一个空位域的A ,是正确的,但我认为不允许使用大小为0的结构。例如,如果要创建该类型的数组,则各个数组元素仍必须具有不同的地址,不是吗?
Jens Gustedt'2

2
它们实际上并不在乎,因为它们使用GNU扩展,它们禁用严格的别名规则,并且不将整数溢出视为UB。但我想知道,这是严格符合C.
ouah

3
有关未命名的零长度位域的@ouah,请参见此处:stackoverflow.com/questions/4297095/…–
David Heffernan

9
@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; })不是(未定义行为)。
哇,2012年

168

有些人似乎将这些宏与混淆assert()

这些宏执行编译时测试,而assert()运行时测试。


52

好吧,我很惊讶没有提到该语法的替代方法。另一种常见的(但较旧的)机制是调用未定义的函数,并且在断言正确的情况下依靠优化器编译出该函数调用。

#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引入了errorfunction属性,该属性允许您扩展这个较旧的概念,但会生成编译时错误,并带有您选择的消息-不再使用神秘的“负大小”数组错误消息!

#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扩展名),您可以!

#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仅为此类型或事物添加了constexpr关键字,但C11中没有对应的关键字。尽管C11确实获得了静态断言,这将解决部分问题,但它无法解决所有这些缺点。因此,我希望gcc可以通过-std = gnuc99和-std = gnuc11或类似的方式使constexpr功能作为扩展使用,并允许其在语句表达式等上使用。等


6
您所有的解决方案都不是替代方案。宏上方的注释非常清楚“ so the expression can be used e.g. in a structure initializer (or where-ever else comma expressions aren't permitted).”宏返回类型的表达式size_t
Wiz13年

3
@Wiz是的,我知道这一点。也许这有点冗长,也许我需要重新审视我的措辞,但是我的观点是探索静态断言的各种机制,并说明为什么我们仍然使用负大小的位域。简而言之,如果我们获得了用于常量语句表达的机制,我们将打开其他选项。
Daniel Santos

无论如何,我们不能将这些宏用作变量。对?error: bit-field ‘<anonymous>’ width not an integer constant它只允许常量。那么,有什么用呢?
Karthik Raj Palanichamy

1
@Karthik搜索Linux内核的源代码以了解使用它的原因。
Daniel Santos

@supercat我完全看不到您的评论如何。您能否对其进行修改,更好地解释您的意思或删除它?
丹尼尔·桑托斯

36

0如果条件为假,则创建一个size 位域,但如果条件为真/非零,则创建一个size -1-!!1)位域。在前一种情况下,没有错误,并且使用int成员初始化了该结构。在后一种情况下,会出现编译错误(-1当然不会创建大小位字段之类的东西)。


3
实际上size_t,如果条件为true,则返回值为0 的a 。
David Heffernan
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.