Is there a type-safe way of getting an element cou

2020-02-12 08:23发布

问题:

The usual approach to getting an array's element count in C in something like this:

#define COUNTOF(arr) (sizeof(arr) / sizeof(arr[0]))

This results in an integral-constant expression, which is a very nice plus as well.

The problem is that it isn't type-safe: int* i; COUNTOF(i); /* compiles :( */. In practice, this should come up rarely, but for the sake of correctness it would be nice to make this type-safe.


In C++03 this is easy (and in C++11 it's even easier, left as an exercise for the reader):

template <typename T, std::size_t N>
char (&countof_detail(T (&)[N]))[N]; // not defined

#define COUNTOF(arr) (sizeof(countof_detail(arr)))

This uses template deduction to get N, the size of the array, then encodes that as the size of a type.

But in C we don't get that language feature. This is the small framework I've made:

// if `condition` evaluates to 0, fails to compile; otherwise results in `value`
#define STATIC_ASSERT_EXPR(condition, value) \
        (sizeof(char[(condition) ? 1 : -1]), (value))

// usual type-unsafe method
#define COUNTOF_DETAIL(arr) (sizeof(arr) / sizeof(arr[0]))

// new method:
#define COUNTOF(arr)                            \
        STATIC_ASSERT_EXPR(/* ??? */,           \
                           COUNTOF_DETAIL(arr)) \

What can I put in /* ??? */ to get my desired behavior? Or is this impossible?

I'd further prefer answers work in MSVC (i.e., C89), but for the sake of curiosity any definite answer will do.

回答1:

This is my second answer. And it gives two solutions.

The first solution requires a gcc extension; the OP did say the he'd prefer answers which work in MSVC, but that "any definite answer will do".

The second solution steals ideas from the excellent answer by ouah https://stackoverflow.com/a/12784339/318716, and is probably more portable.

We start with the classic define:

#define NUMBER_naive(x) ((int)(sizeof(x) / sizeof(x)[0])) // signed is optional

For the first solution, in gcc, you can do a test to determine whether any expression evaluates to an array (or it gives a compile error at (x)[0]); I've tested this solution with the 6-year-old gcc 4.1.2:

#define NUMBER(x) __builtin_choose_expr(                      \
   __builtin_types_compatible_p(typeof(x), typeof((x)[0])[]), \
   NUMBER_naive(x), garbage_never_defined)
extern void *garbage_never_defined;

The second solution is:

#define ASSERT_zero(e) (!sizeof(struct{int:!!(e);})) // BUILD_BUG_ON_ZERO()
#define NUMBER(x) (NUMBER_naive(x) * !ASSERT_zero((void *)&(x) == (x)))

The following is a short test program, on some sample arrays and pointers:

#include <stdio.h>
#define ASSERT_zero(e) (!sizeof(struct{int:!!(e);})) // BUILD_BUG_ON_ZERO()
#define NUMBER_naive(x) ((int)(sizeof(x) / sizeof(x)[0]))
#define NUMBER(x) (NUMBER_naive(x) * !ASSERT_zero((void*)&(x) == (x)))

int a1[10];
extern int a2[];
extern int a3[10];
int *p;
int square[10][10];

static void foo(int param[10]) {
// printf("foo param    %d\n", NUMBER(param));
}
static void bar(int param[][10]) {
// printf("bar param    %d\n", NUMBER(param));
   printf("bar param[0] %d\n", NUMBER(param[0]));
   printf("bar *param   %d\n", NUMBER(*param));
}
int main(void) {
   printf("a1 %d\n", NUMBER(a1));
// printf("a2 %d\n", NUMBER(a2));
   printf("a3 %d\n", NUMBER(a3));
// printf("p  %d\n", NUMBER(p));
   printf("square  %d\n", NUMBER(square));
   printf("*square %d\n", NUMBER(*square));
   foo(a1);
   bar(square);
   return 0;
}

This gives:

a1 10
a3 10
square  10
*square 10
bar param[0] 10
bar *param   10

As you can see, I've commented out four lines which would not or should not compile, three for the three pointers, and one for the incomplete array type.

I had a slight problem with the choice of the third arg of __builtin_types_compatible_p(). The gcc manual (correctly) states "Furthermore, the unused expression (exp1 or exp2 depending on the value of const_exp) may still generate syntax errors." So for now I've set it to a never-instantiated variable, garbage_never_defined, so for some the four commented-out lines, rather than a compile error, we get a compiler warning and a linker error.



回答2:

Example:

#include <stdio.h>

#define IS_NOT_POINTER(x)  (sizeof(x) != sizeof 42[x])
#define COUNTOF(x)         ((int)(sizeof(x) / sizeof 42[x])) // signed is convenient
#define COUNTOF_SAFE(x)    (COUNTOF(x) / IS_NOT_POINTER(x))

extern int x[10];
extern int *y;

int main(void) {
   printf("%d\n", COUNTOF(x));
   printf("%d\n", COUNTOF(y));
   printf("%d\n", COUNTOF_SAFE(x));
   printf("%d\n", COUNTOF_SAFE(y));
   return 0;
}

This gives, with gcc 4.1.2, a compile-time warning:

    foo.c:14: warning: division by zero

Out of curiosity, not that we really care, and is likely to be different from version to version, running it gives:

   10
   1
   10
   0

Edit: I made a slight change to the code, removing IS_NOT_POINTER(x) / IS_NOT_POINTER(x). The compile warning is still there, but now at run-time it gives the correct three values followed by Floating point exception (core dumped). Again, we don't care, but this is probably better.



回答3:

Is there a type-safe way of getting an element count for arrays in C?

I'd say, no. The macro above is nice but it only works correctly when passing a real array.

Macros are only there to simplify your code, you shouldn't relay on them when you want type-safety. If this is what you need, you shouldn't be using C or stick to its rules.