Надежное определение количества элементов в массиве

Каждый программист C может определять количество элементов в массиве с помощью этого известного макроса:

#define NUM_ELEMS(a) (sizeof(a)/sizeof 0[a])

Вот типичный прецедент:

int numbers[] = {2, 3, 5, 7, 11, 13, 17, 19};
printf("%lu\n", NUM_ELEMS(numbers));          // 8, as expected

Однако ничто не мешает программисту случайно передать указатель вместо массива:

int * pointer = numbers;
printf("%lu\n", NUM_ELEMS(pointer));

В моей системе это печатает 2, потому что, по-видимому, указатель в два раза больше целого. Я подумал о том, как не дать программисту пропустить указатель по ошибке, и я нашел решение:

#define NUM_ELEMS(a) (assert((void*)&(a) == (void*)(a)), (sizeof(a)/sizeof 0[a]))

Это работает, потому что указатель на массив имеет то же значение, что и указатель на его первый элемент. Если вы передадите указатель вместо этого, указатель будет сравниваться с указателем на себя, который почти всегда является ложным. (Единственное исключение - это рекурсивный указатель на пустоту, то есть указатель на пустоту, указывающий на себя. Я могу жить с этим.)

Случайное прохождение указателя вместо массива теперь вызывает ошибку во время выполнения:

Assertion `(void*)&(pointer) == (void*)(pointer)' failed.

Ницца! Теперь у меня есть пара вопросов:

  • Я использую assert как левый операнд выражения для запятой, допустимый стандарт C? То есть стандарт позволяет мне использовать assert в качестве выражения? Извините, если это глупый вопрос:)

  • Можно ли как-то выполнить проверку во время компиляции?

  • Мой компилятор C считает, что int b[NUM_ELEMS(a)]; является VLA. Любой способ убедить его в противном случае?

  • Я первый, кто подумал об этом? Если да, то сколько девственников я могу ожидать, ожидая меня на небесах?:)

Ответ 1

Является ли мое использование assert в качестве левого операнда выражения запятой действительным стандартом C? То есть стандарт позволяет мне использовать assert как выражение?

Да, он действителен, так как левый операнд оператора запятой может быть выражением типа void. А функция assert имеет void как возвращаемый тип.

Мой компилятор C считает, что int b [NUM_ELEMS (a)]; является VLA. Любой способ убедить его в противном случае?

Он верит, потому что результат выражения для запятой никогда не является постоянным выражением (e..g, 1, 2 не является постоянным выражением).

EDIT1: добавьте обновление ниже.

У меня есть другая версия вашего макроса, которая работает во время компиляции:

#define NUM_ELEMS(arr)                                                 \
 (sizeof (struct {int not_an_array:((void*)&(arr) == &(arr)[0]);}) * 0 \
  + sizeof (arr) / sizeof (*(arr)))

и, похоже, работает даже с инициализатором для объекта со статической продолжительностью хранения. И он также корректно работает с вашим примером int b[NUM_ELEMS(a)]

EDIT2:

чтобы отправить комментарий @DanielFischer. Макрос выше работает с gcc без -pedantic только потому, что gcc принимает:

(void *) &arr == arr

как целочисленное постоянное выражение, в то время как оно считает

(void *) &ptr == ptr

не является целочисленным постоянным выражением. Согласно C они оба не являются целыми константными выражениями и -pedantic, gcc корректно выдает диагностику в обоих случаях.

Насколько я знаю, нет 100% -ного портативного способа написать этот макрос NUM_ELEM. C имеет более гибкие правила с константными выражениями инициализатора (см. 6.6p7 в C99), которые можно использовать для записи этого макроса (например, с помощью sizeof и сложных литералов), но в блочной области C не требует, чтобы инициализаторы были постоянными выражениями, поэтому невозможно будет иметь один макрос, который работает во всех случаях.

EDIT3:

Я думаю, что стоит упомянуть, что в ядре Linux есть макрос ARRAY_SIZEinclude/linux/kernel.h), который реализует такую ​​проверку, когда выполняется разреженный (проверка статического анализа ядра).

Их решение не переносимо и использует два расширения GNU:

  • typeof оператор
  • __builtin_types_compatible_p встроенная функция

В основном это выглядит примерно так:

#define NUM_ELEMS(arr)  \
 (sizeof(struct {int :-!!(__builtin_types_compatible_p(typeof(arr), typeof(&(arr)[0])));})  \
  + sizeof (arr) / sizeof (*(arr)))

Ответ 2

  • Да. Левое выражение оператора запятой всегда оценивается как выражение void (C99 6.5.17 # 2). Поскольку assert() является выражением void, не стоит начинать с.
  • Может быть. Хотя препроцессор C не знает о типах и методах и не может сравнивать адреса, вы можете использовать тот же трюк, что и для вычисления sizeof() во время компиляции, например. объявляя массив, размер которого является булевым выражением. При 0 это нарушение ограничения и должна быть выдана диагностика. Я пробовал это здесь, но до сих пор не был успешным... может быть, на самом деле ответ "нет".
  • Нет. Casts (типов указателей) не являются целыми константными выражениями.
  • Наверное, нет (ничего нового под солнцем в эти дни). Неопределенное количество девственниц неопределенного пола: -)