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

做什么:-!!做


:是位字段。至于这是逻辑双否定,因此对于false返回0,对于true返回1。-是减号,即算术否定。

这只是一个让编译器在无效输入时停止的技巧。

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

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

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


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


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

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

您应该这样读表达式:

sizeof(struct { int: -!!(e); }))

(e) :计算表达式e。!!(e) :逻辑否定两次:如果e==0,则为0;否则为1。-!!(e) :从数字上否定步骤2中的表达式:如果表达式为0,则为0;否则为-1。结构{int:-!!(0);}-->结构{int:0;}:如果它为零,那么我们声明一个具有宽度为零的匿名整数位字段的结构。一切都很好,我们照常进行。结构{int:-!!(1);}-->结构{int:-1;}:另一方面,如果它不是零,那么它将是一个负数。声明任何宽度为负的位字段都是编译错误。

因此,我们要么在结构中得到宽度为0的位字段(这很好),要么得到宽度为负的位字段,这是一个编译错误。然后我们获取该字段的大小,从而得到具有适当宽度的size_t(在e为零的情况下,它将为零)。


有人问:为什么不使用断言?

keithmo的回答得到了很好的回应:

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

完全正确。您不希望在运行时检测到内核中的问题,而这些问题可能早就被发现了!它是操作系统的关键部分。无论在编译时能检测到什么程度的问题,都会更好。


有些人似乎将这些宏与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_southing_bad()的定义。这就是为什么内核开发人员开始使用负大小的位字段宽度和负大小的数组(后者停止了GCC 4.4中的构建)之类的技巧。

为了同情对编译时断言的需求,GCC 4.3引入了错误函数属性,它允许您扩展这个较旧的概念,但会生成一个编译时错误,并带有您选择的消息——不再是神秘的“负大小数组”错误消息!

#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)

事实上,从Linux3.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或不是编译时常量,则会创建一个编译时错误,显示“我告诉你不要给我一个5!”。

那么为什么我们不使用这个来代替负大小的位字段呢?遗憾的是,当前对语句表达式的使用有许多限制,包括它们作为常量初始化器(用于枚举常量、位字段宽度等)的使用,即使语句表达式本身是完全恒定的(即,可以在编译时完全求值,否则通过__builtin_constant_p()测试)。此外,它们不能在函数体之外使用。

希望GCC能很快修正这些缺点,允许将常量语句表达式用作常量初始化器。这里的挑战是定义什么是合法常量表达式的语言规范。C++11仅为这种类型或事物添加了constexpr关键字,但C11中不存在对应的关键字。虽然C11确实得到了静态断言,这将解决部分问题,但它无法解决所有这些缺点。因此,我希望gcc可以通过-std=gnuc99&-std=gnu11或类似的方式,将constexpr功能作为扩展提供,并允许在语句表达式等上使用它。