如果让你在 C 语言计算一个数组的长度,那么通常的写法是:
#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))
这个确实是行之有效的,但是问题是什么呢?问题是ARRAY_SIZE 的参数必须是一个数组,不能是一个指针,否则就会出现问题。因为 sizeof(一个指向数组的指针) 它的值是固定的(64 位是 8,32 位是 4)。那么怎么避免这个问题的,Linux Kernel 给了一个很好的解决方法,来看 Kernel 版本的 ARRAY_SIZE
:
#define ARRAY_SIZE(arr) (sizeof(arr)/sizeof(arr[0]) + __must_be_array(arr))
后面多了一个对于传入参数的必须是数组的保证,来看看是如何实现的:
/* &a[0] degrades to a pointer: a different type from an array */
#define __must_be_array(a) BUILD_BUG_ON_ZERO(__same_type((a), &(a)[0]))
# define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b))
首先这里用到了一个 Linux Kernel 的一个宏——BUILD_BUG_ON_ZERO
。需要说明的是,这个宏的命名跟他的实际意义是相反的,这个宏的意义是当传入的参数不是 0 的时候抛一个编译错误,否则返回 0,所以曾经有人建议把这个宏改名为 BUILD_BUG_OR_ZERO
。这个命名就是比较贴切的,但是最终并没有被接纳,所以一直在误导新人。
先来看一个这个宏的定义:
/* 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); }))
这里面最重要的知识点就是:逻辑取反和位域(bitfield),其中位域分为两种,命名位域和匿名位域。其中匿名位域长度可以为 0,命名位域必须大于 0。
两次逻辑取反会把非零值变为 1,所以如果 e 为非零值,那么就会是一个长度为 -1 的位域;否则就是一个长度为 0 的匿名位域,对其去 sizeof 就是 0。
这里还用到了 gcc 的一个 built-in function —— __builtin_type_compatible_p
,它的作用是检查两个 type 是否一致(注意是 type,而不是表达式,所以这里用到了 typeof
),一致返回 1,否则返回 0 。并且它检查的是去掉限定词之后的类型,例如 const volatile 等都会被忽略。同时对于它而言,int[] 和 int[5] 类型是一致的;以及两个 enum 将会被认为不是同一个类型。
然后就可以来看它是怎么实现检查参数是数组的。这里用到了 **&a[0]**(说明一下 [] 的优先级比 & 要高)。
- 如果 typeof(a) 是一个数组,此时 typeof(&a[0]) 它就退化成一个指针(因为 a[0] 是一个值,对一个数值取地址就是一个指针了),所以 a 和 &a[0] 不是同一个同类型,__same_type 返回 0,BUILD_BUG_ON_ZERO 返回 0
- 如果 typeof(a) 是一个指针,此时 typeof(&a[0]) 也是一个指针,所以 a 和 &a[0] 是同一个类型,__same_type 返回 1,BUILD_BUG_ON_ZERO 会抛出一个编译错误,这样就能够在编译的阶段就把问题给识别出来,非常高明。
这个是在修一个项目 bug 的时候,当时我用的是第一种写法,然后组内大佬 review 的时候说改成 ARRAY_SIZE,这个是 Kernel 的标准接口,然后去学习了一下深深地被它的优雅给震惊到,所以值得记录一番。
Author: simowce
Permalink: https://blog.simowce.com/Kernel-Magic-ARRAY-SIZE/
本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。