【问题标题】:What is ":-!!" in C code?什么是 ”:-!!”在 C 代码中?
【发布时间】:2012-03-03 00:42:53
【问题描述】:

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

:-!! 是做什么的?

【问题讨论】:

  • - 一元减号
    !逻辑非
    与给定的整数 e 不相反,因此变量可以是 0 或 1。
  • git blame 告诉我们这种特殊形式的静态断言是introduced by Jan Beulich in 8c87df4。显然他有充分的理由这样做(参见提交信息)。
  • 几乎不用说创建的位域是匿名的。这与 C++ 模板元编程的精神相同,即在编译时发生的事情可以在编译时检查。
  • 等等,我以为sizeof 的参数没有被评估。在这种情况下是错的吗?如果是这样,为什么?因为它是一个宏?
  • @cpcloud, sizeof 确实“评估”类型,而不是值。在这种情况下,它的类型无效。

标签: c linux macros linux-kernel


【解决方案1】:

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

宏的名称有些错误;它应该更像BUILD_BUG_OR_ZERO,而不是...ON_ZERO。 (已经有occasional discussions about whether this is a confusing name。)

您应该像这样阅读表达式:

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

  2. !!(e):逻辑否定两次:0 if e == 0;否则1

  3. -!!(e):数值取反第 2 步中的表达式:0,如果它是 0;否则-1

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

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

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


有人问:为什么不直接使用assert

keithmo's answer这里有很好的回应:

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

完全正确。您不想在运行时检测到您的内核 中可能更早发现的问题!它是操作系统的关键部分。在编译时可以检测到任何程度的问题,那就更好了。

【讨论】:

  • C++ 或 C 标准的最新变体有类似 static_assert 的相关用途。
  • @Lundin - #error 需要使用 3 行代码 #if/#error/#endif,并且仅适用于预处理器可访问的评估。此 hack 适用于编译器可访问的任何评估。
  • Linux 内核不使用 C++,至少在 Linus 还活着的时候不会。
  • @Dolda2000:“C 中的布尔表达式被定义为始终计算为零或一”——不完全正确。产生“逻辑布尔”结果的运算符!<><=>===!=&&、@ 987654351@) 总是产生 0 或 1。其他表达式可能产生可用作条件的结果,但仅仅是零或非零;例如,isdigit(c),其中 c 是一个数字,可以产生 any 非零值(然后在条件中被视为真)。
  • 关于名称的快速注释。它被称为...ON_ZERO,因为它是BUG_ON 的派生词,一个本质上是一个断言的宏。 BUG_ON(foo) 的意思是“如果foo 为真,这是一个错误”(在运行时)。相反,BUILD_BUG_ON 是一个静态断言(在构建时检查),最后BUILD_BUG_ON_ZERO 完全相同,只是整个事情是一个等于(size_t)0 的表达式,正如问题中的评论所述。跨度>
【解决方案2】:

: 是一个位域。至于!!,即logical double negation,因此返回0 为false 或1 为true。而- 是减号,即算术否定。

这只是让编译器拒绝输入无效的技巧。

考虑BUILD_BUG_ON_ZERO。当-!!(e) 计算为负值时,会产生编译错误。否则,-!!(e) 的计算结果为 0,宽度为 0 的位域的大小为 0。因此,宏的计算结果为值为 0 的 size_t

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

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

【讨论】:

  • sizeof(struct { int:0; }) 严格符合吗?
  • 为什么结果一般是0struct 只有一个空位域,是的,但我认为不允许使用大小为 0 的结构。例如,如果您要创建该类型的数组,则各个数组元素仍然必须具有不同的地址,不是吗?
  • 他们实际上并不关心,因为他们使用 GNU 扩展,他们禁用严格的别名规则并且不将整数溢出视为 UB。但我想知道这是否严格符合 C.
  • @ouah 关于未命名的零长度位域,请参见此处:stackoverflow.com/questions/4297095/…
  • @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; }) 不是(未定义的行为)。
【解决方案3】:

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

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

【讨论】:

    【解决方案4】:

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

    #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 function attribute,它允许您扩展这个旧概念,但会生成一个编译时错误,并带有您选择的消息——不再神秘“负大小数组”错误消息!

    #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 中的大部分宏都进行了相应的更新。不过,这个宏不能用作初始化器。但是,使用 statement expressions(另一个 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 功能作为扩展提供,并允许将其用于语句表达式等。人。

    【讨论】:

    • 您的所有解决方案都不是替代品。宏上面的注释很清楚“so the expression can be used e.g. in a structure initializer (or where-ever else comma expressions aren't permitted).”宏返回size_t类型的表达式
    • @Wiz 是的,我知道这一点。也许这有点冗长,也许我需要重新审视我的措辞,但我的目的是探索静态断言的各种机制,并说明为什么我们仍在使用负大小的位域。简而言之,如果我们得到了一个常量语句表达的机制,我们就会有其他的选择。
    • 无论如何我们不能将这些宏用于变量。正确的? error: bit-field ‘<anonymous>’ width not an integer constant 它只允许常量。那么,有什么用呢?
    • @Karthik 搜索 Linux 内核的源代码以了解使用它的原因。
    • @supercat 我看不出你的评论有什么关系。您能否修改它,更好地解释您的意思或删除它?
    【解决方案5】:

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

    【讨论】:

    • 实际上,如果条件为真,它会返回一个值为 0 的 size_t
    猜你喜欢
    • 2013-10-25
    • 1970-01-01
    • 1970-01-01
    • 2013-09-22
    • 1970-01-01
    • 1970-01-01
    • 2020-10-31
    • 1970-01-01
    相关资源
    最近更新 更多