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

我在/usr/include/linux/kernel.h中遇到了这个奇怪的宏代码:

/* Force a compilation error if condition is true, but also produce aresult (of value 0 and type size_t), so the expression can be usede.g. in a structure initializer (or where-ever else comma expressionsaren't permitted). */#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!!(e); }))#define BUILD_BUG_ON_NULL(e) ((void *)sizeof(struct { int:-!!(e); }))

:-!!能做什么?

198517 次浏览

:是位域。对于!!,即逻辑双重否定,因此返回0为false或1为true。-是减号,即算术否定。

这只是让编译器在无效输入上呕吐的一个技巧。

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

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

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

如果条件为假,则创建一个size0位字段,如果条件为真/非零,则创建一个size-1-!!1)位字段。在前一种情况下,没有错误并且结构使用int成员初始化。在后一种情况下,存在编译错误(当然不会创建size-1位字段)。

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

这个宏的命名有些错误;它应该更像BUILD_BUG_OR_ZERO,而不是...ON_ZERO。(已经有偶尔讨论这是否是一个令人困惑的名称了。)

你应该阅读这样的表达:

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

  2. !!(e):逻辑上否定两次:0 ife == 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为零的情况下为零)。


有人问:为什么不使用#0?

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_something_bad()的定义。这就是为什么内核开发人员开始使用负大小的位字段宽度和负大小的数组等技巧(后者在GCC 4.4中停止了破坏构建)。

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

#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开始,我们现在有了一个名为#0的宏,它使用了这个特性,#1中的大多数宏都做了相应的更新。尽管如此,这个宏不能用作初始化器。但是,使用语句表达式(另一个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!”。

那么为什么我们不使用this而不是负大小的位域呢?唉,目前语句表达式的使用有很多限制,包括它们作为常量初始化器的使用(用于枚举常量、位域宽度等),即使语句表达式本身是完全恒定的(即,可以在编译时完全评估并通过#0测试)。此外,它们不能在函数体之外使用。

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