什么是 ”:-!!” 用 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); }))

怎么办:-!!做?

答案

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

该宏的名称有些错误;它应该更像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;} :另一方面,如果它为零,则它将为负数。声明任何宽度为负的位字段都是编译错误。

因此,我们要么在结构中使用宽度为 0 的位域(这很好),要么使用宽度为负的位域(这是编译错误)结束。然后,我们使用该字段的sizeof ,从而获得具有适当宽度的size_t (在e为零的情况下为零)。


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

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

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

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

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

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

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

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

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

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

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

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

#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使用这种功能和最 in 的宏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 功能作为扩展使用,并允许其在语句表达式等上使用。等

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