细论数组维度(dimension)的计算
——林石 2008-09-22
我们经常需要知道先前定义的数组维度,或是为了对其进行循环遍历,或是其它。当我们显示初始化数组而没有指定其维度时尤其如此:
- int is[] = {1, 2, 3};
有C语言开发经验的读者可能经常使用如下方式来实现:
- int dimension = sizeof(is) / sizeof(is[0])
这在大部分情况下都工作得很好。只是敲的键盘次数有点多。所以,有了如下这个宏的出现:
- #define DIM(a) (sizeof(a) / sizeof(a[0]))
现在就方便多了。但是依然不完美。考虑下列情况:
- 宏的参数传入一个重载了operator[]操作符的自定义对象
- 宏的参数传入一个指针
我们先看第一种情况。当传入一个重载了operator[]操作的对象时(也许您会说:“等等,我绝对不会这样干的。”可是谁会为您担保呢?),编译器并不会给您报错,甚至吝啬到一条警告都不会给出。不相信我吗?把如下代码片段拷贝到您的IDE中试试吧。
-
- std::vector<int> vi;
- cout << DIM(vi) << endl;
在解决以上这个问题前,我们先插入一点有关C++数组与指针的知识。
很多情况下,C++中的数组可退化为指针。以下便是一个例子:
- int is[] = {1, 2, 3};
- 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宏的定义修改为:
- #define DIM(a) (sizeof(a) / sizeof(0[a]))
第一个问题已被圆满解决。
继续第二个问题。我们需要通过某种机制让编译器能够区分数组与指针,也就是说,当我们传入指针时编译器报错,而传入数组则能正确通过编译。很自然的,我们想到函数调用,借由函数参数来给予区分。像这样:
- template<typename T>
- size_t foo(T *);
- template<typename T>
- size_t foo(T ts[]);
- template<typename T, size_t N>
- inline size_t DimensionOf(T (&ts)[N])
- {
- return N;
- }
现在第一、二个问题都解决了:
- int is[] = {1, 2, 3};
- int *pi = is;
- std::vector<int> vi;
- DimensionOf(is);
- DimensionOf(vi); // Compile-Error
- DimensionOf(pi); // Compile-Error
- template<int N>
- class cls {};
- void f()
- {
- int is[] = {1, 2, 3};
- int is2[foo(is)]; // Compile-Error
- cls<foo(is)> c; // Compile-Error
- }
- template <size_t N>
- struct dimension_help_struct
- {
- unsigned char uc[N];
- };
- template<typename T, size_t N>
- inline const dimension_help_struct<N> make_dimension_help_struct(T (&ts)[N])
- {
- return dimension_help_struct<N>;
- }
- #define DIM(a) (sizeof(make_dimension_help_struct(a)))
为保证 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行前随便敲几个中文,保证您照样能通过编译。
综合以上,最终版本大致是这样:
- template <size_t N>
- struct dimension_help_struct
- {
- unsigned char uc[N];
- };
- template<typename T, size_t N>
- const dimension_help_struct<N>& make_dimension_help_struct(T (&ts)[N]);
- #define DIM(a) (sizeof(make_dimension_help_struct(a).uc))
就在我准备将此文章保存以便第二天提交时突然对上面的方法又有了改进:
- template<typename T, size_t N>
- unsigned char (& dimension_help_fun(T(&ts)[N]))[N];
- #define DIM(a) (sizeof(dimension_help_fun(a)))
(注:以上代码全部在VS2008、GCC4.1.2中测试通过。)
后记:此文从头至尾所讨论的方法便是笔者工作后对数组维度求值所先后采用的方法,笔者愚笨,前后跨越多达3年,这也从侧面反映出C++的复杂性。
<完>