assert
中的((void)0)
assert
是C++开发过程中经常用到的一个宏。在debug模式下,它起到断言的作用;在release模式下,它产生空语句并被编译器优化掉。在<assert.h>
中可以找到它的定义:
#ifdef NDEBUG
#define assert(expression) ((void)0)
#else
// 省略
...
#endif
很多C++初学者对于将assert
定义为((void)0)
表示惊讶和疑惑:((void)0)
是什么意思?有这样的语法吗?为什么要将assert
定义为这样的形式?
以下我将对这些疑问进行分析和解答。
((void)0)
我们首先将目光聚焦到((void)0)
的涵义上。
在宏定义中,为了宏展开的安全,通常使用括号将表达式括起来,所以我们真正需要关注的是(void)0
。从形式上,它很类似于强制类型转换,比如:
int n = 0;
float f = (float) n;
与之类比,似乎(void)0
表示将0
强制转换为void
。那么0
是否可以转换为void
呢?我们求助于C++标准。
在C++11
标准中,有如下说明1:
The
void
type has an empty set of values. Thevoid
type is an incomplete type that cannot be completed. It
is used as the return type for functions that do not return a value. Any expression can be explicitly converted
to type cvvoid
.
可以看到,任何表达式都可以显示地转换为void
类型。而0
事实上是一个表达式,所以(void)0
的涵义就是将表达式0
显示地转换为void
类型。转换前后的区别在于:转换之前,表达式0
的值为int
类型0
;而转换之后,表达式(void)0
的值为void
。
assert
的规则
明白了((void)0)
的涵义后,但是我们的疑问反而加深了:为什么要做这样的转换呢?有什么特殊的意义在里面吗?或者说,为什么不将assert
直接定义为0
或其他的形式呢?
在解开这个疑问前,我们首先要查阅C++标准,以确定assert
的定义需要满足哪些规则。
在C++11
标准中,对于<assert.h>
有这样的说明2:
The contents are the same as the Standard C library header
<assert.h>
.
而在C11
标准中,对于assert
有这样的说明3:
If NDEBUG is defined as a macro name at the point in the source file where
<assert.h>
is included, the assert macro is defined simply as
#define assert(ignore) ((void)0)
以及有这样的规定4:
The
assert
macro returns no value.
可以看到,C++
标准规定,assert
宏必须无返回值,并提出了((void)0)
的形式。所以我们才会在<assert.h>
中看到这样的定义。
于是,我们的问题就转化为:为什么C++
标准要制定这样的规则呢?为什么assert
宏不能有返回值?为什么要是((void)0)
这样的形式?
为什么规定assert
无返回值
首先,assert
起到断言的作用,从逻辑上,它不应该存在返回值;相反地,如果它存在返回值,就可能带来不好的影响。
一般情况下,我们在使用assert
时,是依照这样的方式:
assert(...);
但是,也有可能有人这样使用:
bool b = assert(...);
甚至可以这样用:
doSth(assert(...));
这样的用法明显是不合情理的。所以为了从语法层面上杜绝这种用法,规定assert
必须无返回值,从而就无法作为右值。所以,既然必须得到一个值为void
的表达式,那么使用(void)0
也是再自然不过的事了。
至此,我们就理解了为什么要将0
显式转换为(void)0
,同时也理解了为什么我们不便用诸如((float)0)
、((int)0)
等类似定义。
但是,当我们看到一个宏定义常用的形式时,一个疑问仍然会从我们心底生发出来:为什么不直接将assert
定义为空呢?
为什么assert
不能直接定义为空
assert
既可以在debug模式下使用,也可以在release模式下使用,那么它在两种模式下应该具有一致性:当代码在debug和release模式下切换时,无需手动更改代码,且除assert
以外的代码表现应完全一致。
假如将assert
定义为空,即
#define assert
那么如果有人在debug模式下有如下的代码:
int n = 0;
...
n = doSth(...), assert(0 == n);
在release下就会生成如下代码:
int n = 0;
...
n = doSth(...), ;
从而产生编译错误。
而使用(void)0
将会产生如下代码:
int n = 0;
...
n = doSth(...), (void)0;
这是合法的。孰优孰劣,一眼便知。
可能又有人提出异议:那就规定assert
只能用做语句。这种提议就像是在众多普遍法则中,为assert
专门制定一条特定的专用规则,恰似在一段华丽光滑的绸缎上陡然打上一条补丁,落了下乘。
总结
assert
总体来看,是一个表达式。一方面,为了保持debug和release模式下代码的一致,在release模式下,assert
表达式也必须起到占位的作用(如用作逗号表达式时),所以其不能直接定义为空;另一方面,因为其用作断言功能,理论上不应返回任何值,所以C/C++标准规定其必须返回void
值,从而也防止了其产生副作用。
综上所述,将assert
定义为((void)n)
是最好的方法。而将n
选为0也是再自然不过了。
关于防止警告的意见
有些人5在解释assert
的此种定义时提到,将其定义为((void)0)
而非直接定义为0
,是为了防止产生警告“表达式不起任何作用;应输入带副作用的表达式”。
为此,我使用Visual Stuido 2015 Community版对包含0;
语句的代码进行编译,发现在开启4级(含)及以下警告时,并不会报此警告;只有开启所有警告时才会出现。而“所有警告”在实际开发过程中,是不适用的。所以我对此持保留意见。或许,只是在规定assert
必须返回void
值时,恰好自然地避开了此警告吧。