我的上一篇文章讨论了 Linux 内核C语言源码中的两个宏:
#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!!(e); }))
#define BUILD_BUG_ON_NULL(e) ((void *)sizeof(struct { int:-!!(e); }))
如果读者看了上一篇文章,应该已经明白这两个宏具备编译时检查C语言代码的功能,能够帮助程序员在程序运行之前发现错误。
这两个宏有实用价值吗
BUILD_BUG_ON_XX 宏只能判断常量表达式,似乎没有什么应用价值,毕竟两个常量的对比谁会弄错呢?有读者(@Gerafore)不知道哪种场合用的是,甚至还有读者(@帖木兒)认为这样的宏根本就没有什么用处。
其实 BUILD_BUG_ON_XX 宏就相当于计算器,1 与 0 对比没人弄错,若是常量表达式在复杂一些呢?例如 1234 * 4321 和 2223 * 2322 的对比,如果再复杂些呢?要程序员自己手动去计算这些,就有些丢失编程的意义了,毕竟编程本来是希望计算机处理这些计算的。
另外,在实际的C语言程序开发中,通常都会用到大量的宏,要是每次用到宏,都去翻一翻它的值,再手动计算对比该有多烦啊!
BUILD_BUG_ON_XX 宏的应用还有很多,例如它还可以和一些C语言编译器内置函数结合使用,比如 __builtin_types_compatible_p()
函数,它接收两个数据类型,如果两个数据类型相同,则返回 1,否则返回 0。有趣的是,__builtin_types_compatible_p()
函数返回值是常量,这就为使用类似于BUILD_BUG_ON_XX 的宏提供了条件。
实例
我之前有文章曾讨论过如何使用 sizeof() 关键字计算C语言数组长度:
#define arr_len(arr) (size_t)(sizeof(arr)/sizeof(*arr))
但是需要注意的是,若想 arr_len 宏能够正确计算数组长度,只能传递给它数组名,但是C语言中的数组和指针关系暧昧,很难保证程序员不会误传指针给 arr_len() 宏,例如下面这段C语言代码:
void fun(char a[])
{
...
size_t len = arr_len(a);
...
}
char arr[16] = {0};
fun(arr);
虽然 fun() 函数的参数被写成数组形式(char a[]),但是如果读者看过我之前的文章,应该会明白在 fun() 函数内部,a 其实是会退化成指针的。因此 fun() 函数内部的 arr_len 宏计算 arr 长度时,其实计算的是指针的长度,而不是数组的长度,这就极可能引发 bug,并且这个 bug 会隐藏的比较深,难以发现。
出现这样的问题,是因为 arr_len 宏不能检查传递给自己的是否数组。那有没有办法确保传递给 arr_len 宏的一定是数组呢?自然是有的,结合__builtin_types_compatible_p()
函数和 BUILD_BUG_ON_ZERO 宏就能轻易实现,下面是一个C语言代码示例,请看:
#define must_be_array(a) \
BUILD_BUG_ON_ZERO(__builtin_types_compatible_p(typeof(a), typeof(&a[0])))
在上述C语言代码中,表达式__builtin_types_compatible_p(typeof(a), typeof(&a[0]))
可以判断 a 是否数组,如果 a 是数组,那么 a 的数据类型与 &a[0] 的数据类型不相同,表达式__builtin_types_compatible_p(typeof(a), typeof(&a[0]))
返回 0,按照上一篇文章的分析,BUILD_BUG_ON_ZERO(0)是合法的,可以通过编译。
如果 a 是指针,那么 a 的数据类型与 &a[0] 的数据类型相同,__builtin_types_compatible_p(typeof(a), typeof(&a[0]))
返回 1,BUILD_BUG_ON_ZERO(1)是非法的,在编译阶段就会报错。
must_be_array(a) 宏能够确保 a 一定是数组,否则就会报错,这就为C语言程序提供了安全检查,并且这个检查是在编译时进行的,能够帮助程序员在程序开发阶段发现错误。
现在对 arr_len 宏做修改,相关C语言代码如下,请看:
#define arr_len(arr) \
({ \
must_be_array(arr); \
(size_t)(sizeof(arr)/sizeof(*arr)); \
})
编写相关C语言代码测试之,如下:
#include <stdio.h>
#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!!(e); }))
#define BUILD_BUG_ON_NULL(e) ((void *)sizeof(struct { int:-!!(e); }))
#define must_be_array(a) \
BUILD_BUG_ON_ZERO(__builtin_types_compatible_p(typeof(a), typeof(&a[0])))
#define arr_len(arr) \
({ \
must_be_array(arr); \
(int)(sizeof(arr)/sizeof(*arr)); \
})
int main()
{
int arr1[16] = {0};
int *arr2 = NULL;
printf("%d %d\n", arr_len(arr1), arr_len(arr2));
return 0;
}
编译这段C语言代码,发现编译器报错了:
根据错误信息,很容易确定是 arr2 引发的编译错误。这其实就是 must_be_array 宏的作用了,它发现传递给 arr_len 的不是数组,就会报错。现在将 arr2 也改为数组:
int main()
{
int arr1[16] = {0};
int *arr2[32] = {0};
printf("%d %d\n", arr_len(arr1), arr_len(arr2));
return 0;
}
编译修改后的C语言代码,发现没有错误了,执行之,得到如下输出:
# gcc t2.c
# ./a.out
16 32
可见,arr_len现在安全多了,它能够计算数组长度(包括指针数组),也能够判断传递给自己的究竟是指针还是数组。
小结
本节讨论了上一节介绍的两个宏的实用实例,并给出了一个具体的C语言程序示例,可见,即使自定义的编译时asset只能检查常数表达式,也是用途极大的,很能够帮助C语言程序员开发出更安全的程序。有读者指出,C11 已经原生支持 static_assert(未考证),类似于 BUILD_BUG_ON_XX 的宏已经没有必要存在了。但是遗憾的是,相当多的嵌入式设备并不支持 C11。
另外,我的这两篇文章并不仅仅是讨论 BUILD_BUG_ON_XX 宏,我更希望是向初学者介绍C语言程序开发中的灵活思想。