题 C代码中的“: - !!”是什么?


我碰到了这个奇怪的宏代码 /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); }))

是什么 :-!! 做?


1480
2018-02-10 14:50


起源


@Lundin:git blame告诉我们这种特殊形式的静态断言是 由Jan Beulich在8c87df4中介绍。显然他有充分的理由去做(参见提交消息)。 - Niklas B.
@Lundin:如果 这个 是远离Linux机器的原因,那么请指出一个现代操作系统,其源代码看起来更好。 - Sven Marnach
顺便提一下@SvenMarnach BSD内核(:P)。并不是说我认为BSD内核更好,但为了清洁代码,它们经常牺牲性能(和hackery)。哦,是的,我一直在搞乱BSD和Linux内核代码。 - AoeAoe
@Lundin:对于你来说,BSOD似乎没那么混淆了吗?任何人都可以为Linux做出贡献,那么为什么不放弃而不是远离现代操作系统呢? - yati sagade
@Lundin:assert()不会导致编译时错误。这就是上述结构的重点。 - Chris Pacejo


答案:


实际上,这是 检查表达式e是否可以被评估为0的方法,如果不是,则表示构建失败

这个宏有点名不副实;它应该是更像的东西 BUILD_BUG_OR_ZERO, 而不是 ...ON_ZERO。 (曾经有过 偶尔讨论这是否是一个令人困惑的名字。)

你应该读这样的表达式:

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

  2. !!(e):逻辑否定两次: 0 如果 e == 0;除此以外 1

  3. -!!(e):从数字上否定第2步中的表达式: 0 如果它是 0;除此以外 -1

  4. struct{int: -!!(0);} --> struct{int: 0;}:如果它为零,那么我们声明一个结构,其中包含一个宽度为零的匿名整数位域。一切都很好,我们正常进行。

  5. struct{int: -!!(1);} --> struct{int: -1;}:另一方面,如果它  零,那么它将是一些负数。声明任何位域  width是编译错误。

因此,我们要么结束一个结构中宽度为0的位域,这很好,要么是负宽度的位域,这是一个编译错误。然后我们采取 sizeof 那个领域,所以我们得到一个 size_t 具有适当的宽度(在其中的情况下将为零) e 是零)。


有人问: 为什么不用一个 assert

基思莫的回答 这里有一个很好的回应:

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

非常正确。你不想在你的身上发现问题 核心 在运行时可能早先被抓住了!它是操作系统的关键部分。无论在何种程度上,在编译时都可以检测到问题,那就更好了。


1532
2018-02-10 15:04



最近的C ++或C标准的变体有类似的东西 static_assert 用于相关目的。 - Basile Starynkevitch
@Lundin - #error将需要使用3行代码#if /#error /#endif,并且仅适用于预处理器可访问的评估。此hack适用于编译器可访问的任何评估。 - Ed Staub
Linux内核不使用C ++,至少在Linus还活着时不会。 - Mark Ransom
值得一提的是 !!e 不评估为零或“非零正数”,而是评估为零或一,具体而言。 C中的布尔表达式定义为始终求值为零或一。 - Dolda2000
我也很想知道这一点,特别是当我明确地说0或1时,但我认为你的写作就像一个纯粹的数学家,声称足以证明这一点,并且不过是必要的。 - David Heffernan


: 是一个位域。至于 !!, 那是 逻辑双重否定 所以返回 0 假的或 1 真的。而且 - 是减号,即算术否定。

这只是让编译器对无效输入进行barf的技巧。

考虑 BUILD_BUG_ON_ZERO。什么时候 -!!(e) 求值为负值,产生编译错误。除此以外 -!!(e) 求值为0,0宽度位域的大小为0.因此宏评估为a size_t 值为0。

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

BUILD_BUG_ON_NULL 非常相似,但产生一个指针而不是一个 int


236
2018-02-10 14:54



是 sizeof(struct { int:0; }) 严格遵守? - ouah
为什么结果一般如此 0?一个 struct 只有一个空的位域,为true,但我认为不允许使用大小为0的结构。例如,如果你创建了一个这种类型的数组,那么各个数组元素仍然必须有不同的地址,不是吗? - Jens Gustedt
他们实际上并不关心使用GNU扩展,他们禁用严格的别名规则,不考虑整数溢出为UB。但我想知道这是否严格符合C. - ouah
@ouah关于未命名的零长度位域,请看这里: stackoverflow.com/questions/4297095/... - David Heffernan
@DavidHeffernan实际上C允许未命名的位域 0 width,但如果结构中没有其他命名成员则不会。 (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; }) 不是(未定义的行为)。 - ouah


有些人似乎对这些宏感到困惑 assert()

这些宏实现了编译时测试 assert() 是一个运行时测试。


149
2018-02-10 15:37



@Lundin你在开玩笑吗?显然,你的编译错误要好得多 核心,直到你发射导弹系统引发内核恐慌的巡航导弹,才能让它不被发现。 - John Feminella
@JohnFeminella:是的,我在开玩笑,谁曾经听说过那些真正试图运行他们编写的代码的人!测试 - 哈!它汇编,发货! - Lundin
@Lundin:如果你仍然认为无关紧要,那就没有讽刺和讽刺。这很重要。它很重要,编译错误比运行时错误还要早几英里。如果您认为修复运行时错误所需要的是至少运行一次代码...请参阅上面的评论。 - gparent
@Lundin:实际上我确实理解了这一切,你只是不明白你这么做。无论哪种方式,它在编译时仍然比运行时更好,使得整个论证无关紧要。 - gparent
@Lundin:只有 assert 被执行。我意识到我的测试错过了至少一个代码路径并不罕见。 - Mooing Duck


好吧,我很惊讶没有提到这种语法的替代方案。另一种常见(但更旧)的机制是调用未定义的函数,如果断言正确,则依赖优化器编译函数调用。

#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 已相应更新。但是,此宏不能用作初始化程序。但是,使用by 语句表达式 (另一个GCC C-extension),你可以!

#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功能作为扩展提供,并允许它在语句表达式et上使用。人。


42
2018-06-27 08:21



您的所有解决方案都不是替代方案。宏观上面的评论很清楚“so the expression can be used e.g. in a structure initializer (or where-ever else comma expressions aren't permitted).“宏返回一个类型的表达式 size_t - Wiz
@Wiz是的,我知道这一点。也许这有点冗长,也许我需要重新访问我的措辞,但我的观点是探索静态断言的各种机制,并说明为什么我们仍然使用负大小的位域。简而言之,如果我们得到一个常量语句表达式的机制,我们将打开其他选项。 - Daniel Santos
无论如何,我们不能将这些宏用于变量。对? error: bit-field ‘<anonymous>’ width not an integer constant 它只允许常量。那么,有什么用? - Karthik
@Karthik搜索Linux内核的源代码,了解它的使用原因。 - Daniel Santos


它正在创造一个尺寸 0 如果条件为false,则为bitfield,但是大小 -1 (-!!1)如果条件为真/非零,则为位域。在前一种情况下,没有错误,并且使用int成员初始化struct。在后一种情况下,存在编译错误(并且没有大小这样的东西 -1 当然创建了bitfield)。


31
2018-02-10 14:54



实际上它正在回归 size_t 如果条件为真,则值为0。 - David Heffernan


 Linux Kernel :   

/* 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); }))

-1
2018-06-21 07:18



感谢您提供此代码段,该代码段可能会提供一些有限的即时帮助。一个 适当的解释将大大提高其长期价值 通过展示 为什么 这是解决问题的一个很好的解决方案,并且对于其他类似问题的未来读者来说会更有用。请 编辑 您的答案是添加一些解释,包括您所做的假设。 - Machavity