我們經常需要知道先前定義的數組維度,或是為了對其進行循環遍歷,或是其它。當我們顯示初始化數組而沒有指定其維度時尤其如此:
int is[]={1,2,3};
有C語言開發經驗的讀者可能經常使用如下方式來實現:
int dimension=sizeof(is)/sizeof(is[0])
這在大部分情況下都工作得很好。只是敲的鍵盤次數有點多。所以,有了如下這個宏的出現:
#define DIM(a)(sizeof(a)/sizeof(a[0]))
現在就方便多了。但是依然不完美。考慮下列情況:
宏的參數傳入一個重載了operator[]操作符的自定義對象
宏的參數傳入一個指針
我們先看第一種情況。當傳入一個重載了operator[]操作的對象時(也許您會說:“等等,我絕對不會這樣干的。”可是誰會為您擔保呢?),編譯器並不會給您報錯,甚至吝啬到一條警告都不會給出。不相信我嗎?把如下代碼片段拷貝到您的IDE中試試吧。
1.std::vector<int> vi;
2.cout << DIM(vi) << endl;
“豈有此理,我要把我這該死的編譯器換掉!”您先別急,據我所知,目前還沒有哪家廠商的編譯器會給出錯誤或警告提示,最重要的是,編譯器根本沒有這個責任。
在解決以上這個問題前,我們先插入一點有關C++數組與指針的知識。
很多情況下,C++中的數組可退化為指針。以下便是一個例子:
1.int is[] = {1, 2, 3};
2.int *pi = is;
我們訪問數組時有兩種方式:一種稱為下標式訪問,另一種稱為偏移量訪問。例如,要取得數組is的第二個元素,可分別采用is[1]和*(is + 1),兩種方式等價。實際上,指針也有著同樣的特點,也就是說pi[1]或*(pi + 1)也是取得第二個元素。更有趣的是,C++中的內建(build-in)下標式訪問還可倒過來寫,即is[1]與1[is]等價。吃驚吧。強調一下,這種特性只有在內建的下標式訪問時才正確,換句話說,自定義並重載了operator[]操作符的類型是不具備這種特性的。通過vi[1]方式可取得vector的第二個元素,而當您寫出1[vi]這樣的代碼時編譯器就報錯。
好了,回到我們的問題,我們可以借助上面所提到的C++特性來解決。把DIM宏的定義修改為:
1.#define DIM(a) (sizeof(a) / sizeof(0[a]))
第一個問題已被圓滿解決。
繼續第二個問題。我們需要通過某種機制讓編譯器能夠區分數組與指針,也就是說,當我們傳入指針時編譯器報錯,而傳入數組則能正確通過編譯。很自然的,我們想到函數調用,借由函數參數來給予區分。像這樣:
1.template<typename T>
2.size_t foo(T *);
3.template<typename T>
4.size_t foo(T ts[]);
很遺憾,這兩個函數簽名對於編譯器來說沒有兩樣,您的編譯器會提示您重復定義。別灰心,其實已經很接近了。稍微修改一下:
1.template<typename T, size_t N>
2.inline size_t DimensionOf(T (&ts)[N])
3.{
4. return N;
5.}
我們定義一個模板函數,接收一個數組引用,其中T為數組元素類型,N是數組維度,為提高效率,定義成inline形式。編譯器會幫我們把N推導出來,非常感謝它。
現在第一、二個問題都解決了:
1.int is[] = {1, 2, 3};
2.int *pi = is;
3.std::vector<int> vi;
4.DimensionOf(is);
5.DimensionOf(vi); // Compile-Error
6.DimensionOf(pi); // Compile-Error
因為是個函數,所以可以置於名字空間內,而inline形式的調用開銷可被忽略不計。非常好,可不完美,因為調用結果不是編譯期常量。我們不能這樣使用:
1.template<int N>
2.class cls {};
3.
4.void f()
5.{
6. int is[] = {1, 2, 3};
7. int is2[foo(is)]; // Compile-Error
8. cls<foo(is)> c; // Compile-Error
9.}
利用sizeof操作是在編譯期而非運行期求值的事實,我們可再修改成如下:
1.template <size_t N>
2.struct dimension_help_struct
3.{
4. unsigned char uc[N];
5.};
6.
7.template<typename T, size_t N>
8.inline const dimension_help_struct<N> make_dimension_help_struct(T (&ts)[N])
9.{
10. return dimension_help_struct<N>;
11.}
12.
13.#define DIM(a) (sizeof(make_dimension_help_struct(a)))
首先定義了一個輔助模板結構體dimension_help_struct,我們期望模板參數N即為結構體的大小,即N == sizeof(dimension_help_struct<N>)恆成立,然後定義了一個模板函數make_dimension_help_struct,讓編譯器推導出數組ts的維度並生成一個dimension_help_struct對象,最後定義一個DIM宏。
為保證N == sizeof(dimension_help_struct<N>)成立,我們得保證編譯器對dimension_help_struct對象使用1byte字節對齊。更精確的辦法是對結構體dimension_help_struct加以#pragma pack(1)指令。但是我們有更簡單的辦法:只要確保N == sizeof(dimension_help_struct<N>.uc)恆成立即可。
此外,模板函數make_dimension_help_struct的定義體根本不需要,因為sizeof是編譯期求值,用不著函數調用。不相信的話在第10行前隨便敲幾個中文,保證您照樣能通過編譯。
綜合以上,最終版本大致是這樣:
1.template <size_t N>
2.struct dimension_help_struct
3.{
4. unsigned char uc[N];
5.};
6.
7.
8.template<typename T, size_t N>
9.const dimension_help_struct<N>& make_dimension_help_struct(T (&ts)[N]);
10.
11.
12.#define DIM(a) (sizeof(make_dimension_help_struct(a).uc))
因為所有步驟都在編譯期求值,所以無任何性能損耗。唯一不好的一點是DIM宏,不能將其置入名字空間內。
就在我准備將此文章保存以便第二天提交時突然對上面的方法又有了改進:
1.template<typename T, size_t N>
2.unsigned char (& dimension_help_fun(T(&ts)[N]))[N];
3.#define DIM(a) (sizeof(dimension_help_fun(a)))
您沒看錯,這就是全部代碼。把前一種方法的輔助結構體都省掉了,只剩下一個輔助模板函數,這個函數接收一個數組引用ts,返回一個unsigned char型並具有N個元素的數組引用。
(注:以上代碼全部在VS2008、GCC4.1.2中測試通過。)
後記:此文從頭至尾所討論的方法便是筆者工作後對數組維度求值所先後采用的方法,筆者愚笨,前後跨越多達3年,這也從側面反映出C++的復雜性。