static_assert是我認為boost庫中依賴性最小的一個組件。當然對於很多人來說,assert的習慣都還未必能養成,更莫說static_assert,我就還是講解一下。
static_assert:如果傳入一個false或者0, 就拋出一個編譯錯誤。既然是要激發編譯時間的錯誤,那麼只能傳入字面常量了。不過就算是只能檢查常量的值,也可以檢查很多東西了。另外他還能起到在編譯錯誤時,提前阻止模板的深度展開,使模板編譯錯誤資訊易於理解
如何把一個false或者0變成編譯錯誤,筆者先拋磚引玉,給一個例子。
#define STATIC_ASSERT(N) int A[ (bool(N) ? 1 : -1) ]
//顯然當N= false或者0時,STATIC_ASSERT(N)等價於定義了一個-1長度的數組,gcc雖然支援0長度數組,負數長度顯然不支援,所以所有的編譯器都立即報錯.
不過至少還有2個問題要解決:定義數組顯然會帶來空間開銷,其次名字A在一個名字空域內會重複.
第2個問題的解決方案很簡單,把名字A拼接上行數就行了,A##__LINE__. 只要你不在一行或一個宏中反覆展開STATIC_ASSERT,行數就是不會重複的.
第1個問題的解決要考慮到方法要能在類名字空間域,全域名字空間域,函數名字空間域等等場所適用.下面有至少這幾種方法在所有編譯器中通行:
#define STATIC_ASSERT(N) typedef int A[ (bool(N) ? 1 : -1) ]
#define STATIC_ASSERT(N) void A( int (*arg)[ (bool(N) ? 1 : -1) ])
#define STATIC_ASSERT(N) enum {A = sizeof(int [ (bool(N) ? 1 : -1) ])}
上述方法都沒有任何時空開銷(無論是debug 版或者release版)
注意有的編譯器支援 C99的變長數組,如果誤傳入的N是一個變數,#define STATIC_ASSERT(N) int A[ (bool(N) ? 1 : -1) ];會導致執行階段錯誤.後面列舉的3種方法都有效防治了這個錯誤.檢查變數請使用assert.h中的assert.
定義的方法差不多都說完了,我們回到原來的問題,如何把一個false或者0變成編譯錯誤.遠遠不止有筆者剛才提到的負數長度數組這樣的錯誤.
筆者把一些常見的激發編譯錯誤的方法列舉一下,為了方便和易於閱讀,通通使用數組法來進行表示:
可以相容C/C++語言的方法:
錯誤1:負數長度的數組, #define STATIC_ASSERT(N) int A[ (bool(N) ? 1 : -1) ]; //錯誤1
錯誤2:進行除"0"常數算術運算, #define STATIC_ASSERT(N) int A[ (int(N) / int(N)) ]; //錯誤2
錯誤3:函數調用參數不匹配.如下:
size_t ttt(size_t(*arg)[ size_t(true) + 1 ]);
#define STATIC_ASSERT(N) int A[sizeof(ttt(size_t(*)[ size_t(N) + 1 ](NULL)))]
筆者挖空心思,在c語言下對常數輸入的限制就只找到這3個了.第3個其實已經很勉強了,另外利用邏輯運算短路特性在後半段塞入語法錯誤的通用性不好. 歡迎大家集思廣益,多多賜教!
只能用在c++的方法,自然都是和模板相關的了,利用模板參數可以是常整數這點.一般手法都是通過只特化模板參數值為true或者false的類來激發訪問錯誤. 往往篇幅過長,筆者就不贅述了:
錯誤3:試圖執行個體化一個已經聲明,但沒有定義的類.c++ modern design和boost就用的是這個.
錯誤4:試圖訪問一個不存在的類子定義或者靜態常量.
錯誤5:試圖無限遞迴執行個體化.
錯誤6:試圖讓某類自己繼承自己.
錯誤7:試圖讓某類繼承另外一個類2次. 如struct A: public B, public B{};
錯誤8:試圖讓某個對象轉換成一個非父類的類型.
錯誤9:試圖讓某類擁有自己類型的成員.
........實在是太多了,我想c++編譯器作者比我更有發言權,c++就是一種給你無限種可能的語言.
事實上,很多static_assert還希望能夠在編譯錯誤中顯示定製的特定資訊,自然又得在名字A上面下功夫了,而且顯然只能讓這些資訊添加在變數名字或者自訂類型名字中, 利用編譯錯誤會列印這些變數或者類型的資訊.並且這些資訊中不能有中文,不能有除了'_'以外的一切標點,不能有空格.這裡筆者貼上自己的代碼給大家看看吧.
注意我把產生錯誤, 錯誤的利用, 訊息的格式幾個部分分開在幾個宏裡了.另外把調試版和非調試版分開了.
#define JOIN(x,y) x ## y<br />#define JOIN_1(x,y) JOIN(x, y)<br />#define JOIN_2(x,y) JOIN_1(x, y)<br />/*It's the begin of all error message.*/<br />#define ERRORMSG_BEGIN ATTENTION__The_assertion_error_is__<br />/*<br />*To fetch a error if testnum==0||testnum==false, by divide 0.<br />*Cast to int to avoid warning caused by bool division.<br />*/<br />#define IF_ERROR(testnum) (((int)(testnum))/((int)(testnum)))<br />/*<br />*The errormsg only support following characters: [a-zA-Z0-9] and '_'.<br />*Because it's a part of variable's name.<br />*Example:STATIC_ASSERT(sizeof(int) <= sizeof(short), The_sizeof_int_is_larger_than_short__)<br />*For above example, (Most) compilers can print error include following message:<br />ATTENTION__The_assertion_error_is__The_sizeof_int_is_larger_than_short__234<br />*The number in the end is the linenumber of the position of the macro been called.<br />*/<br />#ifndef NDEBUG<br />#ifndef _MSC_VER<br />#define STATIC_ASSERT(testnum, errormsg) static char JOIN_2(JOIN_1(ERRORMSG_BEGIN,errormsg),__LINE__)[IF_ERROR(testnum)]<br />#else<br />#define STATIC_ASSERT(testnum, errormsg) static char JOIN_1(ERRORMSG_BEGIN,errormsg)[IF_ERROR(testnum)]<br />#endif<br />#else<br />#define STATIC_ASSERT(testnum, errormsg)<br />#endif
實際的編譯錯誤中往往只有定義了一個變數,(多數)編譯器的才肯列印它的名字.typedef,或者函式宣告似乎都不會列印對應的名字,所以只好理論服從實際咯.