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.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章