不使用宏定义编译时计算数组大小

16

这只是最近几天困扰我的问题,我认为它是无法解决的,但我曾见过模板魔法。

以下是两种获取标准 C++ 数组元素数量的方法:使用宏(1)或类型安全的内联函数(2):

(1)

#define sizeof_array(ARRAY) (sizeof(ARRAY)/sizeof(ARRAY[0]))

(2)

template <typename T>
size_t sizeof_array(const T& ARRAY){
    return (sizeof(ARRAY)/sizeof(ARRAY[0]));
}

正如您所看到的,第一个存在宏问题(目前我认为这是个问题),而另一个则存在不能在编译时获取数组大小的问题;即我无法编写:

enum ENUM{N=sizeof_array(ARRAY)};
或者
BOOST_STATIC_ASSERT(sizeof_array(ARRAY)==10);// Assuming the size 10..

有人知道这个问题能否解决吗?

更新:

这个问题是在constexpr被引入之前创建的。现在你可以简单地使用:

template <typename T>
constexpr auto sizeof_array(const T& iarray) {
    return (sizeof(iarray) / sizeof(iarray[0]));
}

2
可能是重复的问题?https://dev59.com/9UXRa4cB1Zd3GeqPpzQi - Rob
10个回答

23

请尝试从这里尝试以下内容:

template <typename T, size_t N>
char ( &_ArraySizeHelper( T (&array)[N] ))[N];
#define mycountof( array ) (sizeof( _ArraySizeHelper( array ) ))

int testarray[10];
enum { testsize = mycountof(testarray) };

void test() {
    printf("The array count is: %d\n", testsize);
}

它应该输出:“数组计数为:10”

注意--针对您的两个问题:#1) 宏:您不必使用countof()宏,只需使用sizeof(_ArraySizeHelper(array))即可,但countof()宏是一种简化。#2) 枚举--在上面的示例中,它适用于枚举。 - Adisak
3
这样做的额外好处是类型安全,即如果您传递指针而不是数组,则编译会失败。 - Josh Kelley
3
这是一个不错的解决方案,但并非完全通用:“它无法处理在函数内定义的类型”。 - Georg Fritzsche
2
@ gf:通常情况下,模板对于在函数内定义的类型效果不佳。事实上,它对STL也不起作用——在GCC下,以下代码将无法编译:void test() { struct foo{ int a; }; std::vector<foo> b; } 如果您需要创建一个仅由一个函数使用并且需要在该类上使用模板的类,则最好使用命名空间(或匿名命名空间)。 - Adisak
@Adisak,@Georg:我认为有一种方法可以使本地类型正常工作:https://dev59.com/5HI_5IYBdhLWcg3wK_o6#6256085 - Nate Kohl

19
在C++1x中,constexpr可以帮助你实现这个目标:
template <typename T, size_t N>
constexpr size_t countof(T(&)[N])
{
    return N;
}

1
这个代码也可以用C++99编写,在大多数情况下,编译器会优化掉这个调用。 - David Rodríguez - dribeas
5
但在 C++98 中,它不能用于需要常量表达式的地方。 - jalf
是的...在枚举上不起作用--至少在我目前用于工作的任何C++编译器中都不起作用,这些编译器都是C++1X之前的版本。 - Adisak

8
我能想到的最好的方案是这样的:
template <class T, std::size_t N>
char (&sizeof_array(T (&a)[N]))[N];

// As litb noted in comments, you need this overload to handle array rvalues
// correctly (e.g. when array is a member of a struct returned from function),
// since they won't bind to non-const reference in the overload above.
template <class T, std::size_t N>
char (&sizeof_array(const T (&a)[N]))[N];

这必须与另一个sizeof一起使用:

int main()
{
    int a[10];
    int n = sizeof(sizeof_array(a));
    std::cout << n << std::endl;
}

[编辑]

仔细想想,我相信在C++03中单个“函数式调用”实现这个是不可能的,除了宏以外,原因如下。

一方面,你显然需要模板参数推导来获取数组的大小(直接或通过sizeof),但是模板参数推导只适用于函数,而不适用于类。也就是说,你可以有一个类型为引用到长度为N的数组的模板参数R,其中N是另一个模板参数,但是你必须在调用时同时提供R和N;如果你想从R中推导出N,只有函数调用才能做到。

另一方面,任何涉及函数调用表达式的常量表达式只有在sizeof内部时才是常量。其他任何操作(例如在函数返回值上访问静态或枚举成员)仍然需要进行函数调用,这显然意味着这不会是一个常量表达式。


去掉 const 使它更加通用。 - dalle
谢谢你的提示。如果它得到一个const U[N]&作为参数,它将能够推断出Tconst U吗? - Pavel Minaev
我喜欢在这些情况下使用identity,因为它可以极大地提高可读性:template<typename T, size_t N> typename identity<char[N]>::type &sizeof_array(T const(&)[N]);。对于“const”的删除,实际上降低了泛型性。因为现在你不能再接受rvalue数组了(非const数组引用将无法绑定到rvalue数组):struct A { int b[2]; }; .. sizeof_array(A().b); // 如果不是const,则无法工作!。请注意,这个rvalue数组本身并不是const,因此推导也不会放置const。请注意,这不是一个大问题,但可以说它更不够泛化。 - Johannes Schaub - litb
是的,关于rvalue数组你说得对...看起来这确实需要两个重载才能完全通用。我会添加的。 - Pavel Minaev
1
请注意,T const(&)[N] 接受非 const 数组也是可以的。模板类型推导会正常推导出 T,不会被额外的 const 所干扰。这样做有助于推广 const 的使用——如果模板类型推导要求人们在接受非 const 参数时省略 const,那就不好了。:) 无论如何,我点赞你的分析和合理的命名(没有疯狂的下划线之类的 xD)。 - Johannes Schaub - litb
显示剩余2条评论

6

问题

我喜欢Adisak的回答

template <typename T, size_t N>
char ( &_ArraySizeHelper( T (&arr)[N] ))[N];
#define COUNTOF( arr ) (sizeof( _ArraySizeHelper( arr ) ))

这是微软在VS2008中使用的_countof宏,它有一些不错的特性:

  • 它在编译时操作
  • 它是类型安全的(即,如果你给它一个指针,它会生成一个编译时错误,而数组则很容易退化为指针)

但正如Georg所指出的,这种方法使用了模板,因此不能保证对于C++03中的本地类型有效

void i_am_a_banana() {
  struct { int i; } arr[10];
  std::cout << COUNTOF(arr) << std::endl; // forbidden in C++03
}

幸运的是,我们没有失去机会。

解决方案

Ivan Johnson提出了一种聪明的方法(链接),在所有方面都胜出:它是类型安全的、编译时的,并且适用于本地类型:

#define COUNTOF(arr) ( \
   0 * sizeof(reinterpret_cast<const ::Bad_arg_to_COUNTOF*>(arr)) + \
   0 * sizeof(::Bad_arg_to_COUNTOF::check_type((arr), &(arr))) + \
   sizeof(arr) / sizeof((arr)[0]) )

struct Bad_arg_to_COUNTOF {
   class Is_pointer; // incomplete
   class Is_array {};
   template <typename T>
   static Is_pointer check_type(const T*, const T* const*);
   static Is_array check_type(const void*, const void*);
};

对于那些感兴趣的人,它是通过在标准的基于sizeof的数组大小宏之前插入两个“测试”来工作的。这些测试不会影响最终的计算,但旨在为非数组类型生成编译错误:

  1. 第一个测试失败,除非 arr 是整数、枚举、指针或数组。对于其他类型,reinterpret_cast<const T*> 应该失败。
  2. 第二个测试失败,对于整数、枚举或指针类型。

    整数和枚举类型将失败,因为它们没有与之匹配的 check_type 版本,因为 check_type 期望指针。

    指针类型将失败,因为它们将匹配模板化的 check_type 版本,但模板化 check_type 的返回类型(Is_pointer)是不完整的,这将产生错误。

    数组类型将通过,因为取类型为 T 的数组的地址将给出 T (*)[],即指向数组的指针,而不是指向指针的指针。这意味着模板化的 check_type 版本不会匹配。由于 SFINAE,编译器将转向非模板化的 check_type 版本,它应该接受任何一对指针。由于非模板化版本的返回类型已完全定义,因此不会产生错误。而且,由于我们现在不涉及模板,本地类型可以正常工作。


这不是模板实现,但这是我目前找到的最好的。 - To1ne
我喜欢Ivan Johnson的想法,但我有点困惑。在C++11中有更简单的解决方案,所以让我们假设我们没有那个。然后模板不会捕捉到局部类,所以localClass x; std::size_t length = COUNTOF(&x);实际上会编译吗?(抱歉,我手头没有实现那个旧“特性”的编译器。) - Joshua Green
我可能错了。调用COUNTOF(&x)可能会导致尝试使用localClass作为template参数来实例化Is_pointer check_type,从而可能导致编译时错误(而不仅仅是SFINAE)。我无法确定这是否先前由标准保证或只是不清楚并被假定。 - Joshua Green
我在g++上进行了测试。在C++11模式下,一切似乎都正常工作,但在非C++11模式下,g++很“高兴”地编译了LocalType x; LocalType *ptr = &x; std::size_t length = COUNTOF(ptr);,其中length得到了一个完全无意义的值。无论这是一种“扩展”,还是对模糊标准的有效解释,或者是一个错误,对我来说都不清楚,但无论如何,在C++11之前,似乎都无法实现一个完全类型安全的处理本地类型的版本。 - Joshua Green

6

这不完全是你要找的,但它很接近 - 来自winnt.h的片段,其中包含了一些解释其操作的内容:

//
// RtlpNumberOf is a function that takes a reference to an array of N Ts.
//
// typedef T array_of_T[N];
// typedef array_of_T &reference_to_array_of_T;
//
// RtlpNumberOf returns a pointer to an array of N chars.
// We could return a reference instead of a pointer but older compilers do not accept that.
//
// typedef char array_of_char[N];
// typedef array_of_char *pointer_to_array_of_char;
//
// sizeof(array_of_char) == N
// sizeof(*pointer_to_array_of_char) == N
//
// pointer_to_array_of_char RtlpNumberOf(reference_to_array_of_T);
//
// We never even call RtlpNumberOf, we just take the size of dereferencing its return type.
// We do not even implement RtlpNumberOf, we just decare it.
//
// Attempts to pass pointers instead of arrays to this macro result in compile time errors.
// That is the point.
//
extern "C++" // templates cannot be declared to have 'C' linkage
template <typename T, size_t N>
char (*RtlpNumberOf( UNALIGNED T (&)[N] ))[N];

#define RTL_NUMBER_OF_V2(A) (sizeof(*RtlpNumberOf(A)))
RTL_NUMBER_OF_V2()宏最终被用于更易读的ARRAYSIZE()宏。 Matthew Wilson的"Imperfect C++"书中也讨论了这里使用的技术。

+1:阅读代码的计数使我感到困惑(我尽量不花太多时间掌握C++从C继承的晦涩声明规则)。这个答案提供了有关RtlpNumberOf的确切信息。 - paercebal

4
如果您正在使用仅限于Microsoft平台,您可以利用_countof宏。这是一个非标准扩展,它将返回数组中元素的计数。与大多数countof样式宏相比,它的优势在于,如果它用于非数组类型,它将导致编译错误。以下内容在VS 2008 RTM中完美运行。
static int ARRAY[5];
enum ENUM{N=_countof(ARRAY)};

但是再次强调,这是Microsoft特定的,所以可能不适用于您。

countof在C中实现为宏,在C++中实现为类型安全的内联函数;换句话说,它与问题中的(1)和(2)完全相同。 - Josh Kelley

3

一般情况下无法解决此问题,这也是像boost array这样的数组包装器存在的原因(当然还有STL式的行为)。


-1:错误。模板元编程可以解决它,甚至在C++11之前就可以了。 - Thomas Eding
@ThomasEding:请详细说明一下在C++11之前如何解决这两个具体的问题,而不使用宏。原问题是关于如何在不使用宏的情况下解决它们。 - Georg Fritzsche
观察Nate Kohls的回答,例如:https://dev59.com/5HI_5IYBdhLWcg3wK_o6#6256085。(他使用了一个宏,但那只是方便使用;不是必须的。)我没有尝试过,但我相信在C++03中模拟`decltype`应该是可能且相当简单的,因此你可以编写相当于C++11中`std::extent<std::enable_if<std::is_array<decltype(x)>::value, decltype(x)>::type>::value`的代码。是的,这些特性在C++03中不存在,但它们肯定是可实现的。 - Thomas Eding
如果它能够正确地处理本地类型,那么这个答案很有趣,但是没有宏,它实际上是无法使用的(也就是说,在实践中你不会使用它)。在C++03中模拟decltype是不可行的。此外,在C++03中缩短像上面那样的表达式以便使用听起来是不现实的。 - Georg Fritzsche
@George:你说得对。我曾经认为在C++(甚至是C++11)中可以做某些事情,但当我尝试编写一些代码时发现根本不可能实现。看来我无法取消我的投票...哇。 - Thomas Eding
没问题,您的投票会被锁定直到下一次编辑(我更担心可能错过了什么)。 - Georg Fritzsche

2

现在STL库可以在编译时决定/选择数组大小

#include <iostream>
#include <array>

template<class T>
void test(T t)
{
    int a[std::tuple_size<T>::value]; // can be used at compile time
    std::cout << std::tuple_size<T>::value << '\n';
}

int main()
{
    std::array<float, 3> arr;
    test(arr);
}

输出结果: 3


你的水平太低了。顺便说一下,现在是C++11及以上版本。 - Martin Morterol

2

如果没有 C++0x,最接近的方法是:

#include <iostream>

template <typename T>
struct count_of_type
{
};


template <typename T, unsigned N>
struct count_of_type<T[N]> 
{
    enum { value = N };
};

template <typename T, unsigned N>
unsigned count_of ( const T (&) [N] )
{
    return N;
};


int main ()
{
    std::cout << count_of_type<int[20]>::value << std::endl;
    std::cout << count_of_type<char[42]>::value << std::endl;

    // std::cout << count_of_type<char*>::value << std::endl; // compile error

    int foo[1234];

    std::cout << count_of(foo) << std::endl;

    const char* bar = "wibble";

    // std::cout << count_of( bar ) << std::endl; // compile error

    enum E1 { N = count_of_type<int[1234]>::value } ;

    return 0;
}

这个函数要么给你一个可以传递变量的函数,要么给你一个可以传递类型的模板。你不能将函数用于编译时常量,但大多数情况下你知道类型,即使只是作为模板参数。


使用C++0x,如果您将其声明为“constexpr”,则可以将函数用作编译时常量 - 正如其他答案所指出的那样。 - Pavel Minaev
阅读了那篇文章后,我在我的文章开头加上了“没有C++0x”,以表明我没有使用那种技术,而是使用了一种适用于当前编译器的技术。 - Pete Kirkham
编译时常量版本看起来有点毫无意义。如果您已经知道大小为20,为什么还要费力地输入count_of_type <int [20]> ::value呢? :) - UncleBens
你可能需要使用模板参数T来获取count_of_type<T>::value,但我同意这样的情况可能很少见。 - Pete Kirkham

2
目前根据C++标准,似乎无法在没有宏的情况下获得数组大小的编译时常量(您需要一个函数来推断数组大小,但是在需要编译时常量的地方不允许函数调用)。[编辑:但请参见Minaev的杰出解决方案!]
然而,您的模板版本也不是类型安全的,并且遇到了与宏相同的问题:它也接受指针,尤其是衰减为指针的数组。当它接受一个指针时,sizeof(T *)/ sizeof(T)的结果可能没有意义。
更好的方式是:
template <typename T, size_t N>
size_t sizeof_array(T (&)[N]){
    return N;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接