assert()中的(void(0))浅析

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. The void 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 cv void.

可以看到,任何表达式都可以显示地转换为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值时,恰好自然地避开了此警告吧。

参考链接

  1. ((void) 0) 这是什么用法?
  2. What does “#define assert(exp) ((void) 0)” do?
  3. casting 0 to void
  4. Why is (void) 0 a no operation in C and C++?

  1. C++11标准 ISO/IEC 14882:2011(E) 3.9.1(9);
  2. C++11标准 ISO/IEC 14882:2011(E) 19.3(2);
  3. C11标准 ISO/IEC 9899:201x(草案) 7.2.1.1(2);
  4. C11标准 ISO/IEC 9899:201x(草案) 7.2(1);
  5. What does “#define assert(exp) ((void) 0)” do? Steve Jessop’s answer.
  • 22
    点赞
  • 57
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值