Существует ли безопасный тип получения элемента для массивов в C?

Обычный подход к подсчету элемента массива в C выглядит примерно так:

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

Это приводит к выражению интегральной константы, что тоже очень приятно.

Проблема в том, что она не безопасна для типов: int* i; COUNTOF(i); /* compiles :( */. На практике это должно возникать редко, но для правильности было бы неплохо сделать этот тип безопасности.


В С++ 03 это легко (и в С++ 11 это еще проще, оставлено как упражнение для читателя):

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

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

Это использует вывод шаблона, чтобы получить N, размер массива, а затем кодирует его как размер типа.

Но в C мы не получаем эту функцию языка. Это небольшая рамка, которую я сделал:

// 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)) \

Что я могу добавить в /* ??? */, чтобы получить желаемое поведение? Или это невозможно?

Я бы предпочел, чтобы ответы работали в MSVC (т.е. C89), но ради любопытства любой конкретный ответ будет делать.

Ответ 1

Это мой второй ответ. И это дает два решения.

Первое решение требует расширения gcc; ОП сказал, что он предпочел бы ответы, которые работают в MSVC, но что "любой конкретный ответ будет".

Второе решение ворует идеи из превосходного ответа oaah fooobar.com/questions/23776/... и, вероятно, более портативен.

Начнем с классического определения:

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

Для первого решения в gcc вы можете выполнить тест, чтобы определить, оценивает ли какое-либо выражение массив (или он дает ошибку компиляции в (x)[0]); Я протестировал это решение с 6-летним 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;

Второе решение:

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

Ниже приведена короткая тестовая программа на некоторых массивах образцов и указателях:

#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;
}

Это дает:

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

Как вы можете видеть, я прокомментировал четыре строки, которые не должны или не должны компилироваться, три для трех указателей и один для неполного типа массива.

У меня была небольшая проблема с выбором третьего arg __builtin_types_compatible_p(). gcc manual (правильно) указывает "Furthermore, the unused expression (exp1 or exp2 depending on the value of const_exp) may still generate syntax errors." Так что на данный момент я установил его для никогда не созданной переменной garbage_never_defined, поэтому для некоторых четырех пронумерованных строк, а не ошибки компиляции, мы получаем предупреждение компилятора и ошибку компоновщика.

Ответ 2

Пример:

#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;
}

Это дает, с gcc 4.1.2, предупреждение времени компиляции:

    foo.c:14: warning: division by zero

Из любопытства, а не того, что нам действительно важно, и, вероятно, будет отличаться от версии к версии, запуск его дает:

   10
   1
   10
   0

Изменить: Я немного изменил код, удалив IS_NOT_POINTER(x) / IS_NOT_POINTER(x). Предупреждение компиляции все еще существует, но теперь во время выполнения он дает правильные три значения, за которыми следует Floating point exception (core dumped). Опять же, нам все равно, но это, вероятно, лучше.

Ответ 3

Существует ли безопасный тип получения количества элементов для массивов в C?

Я бы сказал, нет. Макрос выше хорош, но он работает корректно только при передаче реального массива.

Макросы только там, чтобы упростить ваш код, вы не должны ретранслировать их, когда вам нужна безопасность типов. Если это то, что вам нужно, вы не должны использовать C или придерживаться своих правил.