关于linux:C代码中的:!!是什么?

What is “:-!!” in C code?

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

:-!!是做什么的?


实际上,这是一种检查表达式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.

    完全正确。您不希望在运行时检测内核中可能在早期被捕获的问题!它是操作系统的关键部分。不管在编译时能在多大程度上检测到问题,都会更好。


    :是一个位字段。对于!!,这是逻辑双否定,因此返回0表示假,返回1表示真。-是一个负号,即算术否定。

    这只是让编译器在无效输入上死记硬背的一个技巧。

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

    在我的视图中,这个名称很弱,因为当输入不是零时,构建实际上会失败。

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


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

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


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

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


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