Массив размера массива, который отклоняет указатели
Обычный макет размера массива, который часто изучается,
#define ARRAYSIZE(arr) (sizeof(arr) / sizeof(arr[0]))
или некоторой эквивалентной формации. Тем не менее, этот вид беззвучно преуспевает, когда передается указатель, и дает результаты, которые могут показаться правдоподобными во время выполнения, пока вещи не таинственно разваливаются.
Слишком легко сделать эту ошибку: функция, которая имеет переменную локального массива, реорганизуется, перемещая немного манипуляции с массивом в новую функцию, называемую массивом в качестве параметра.
Итак, возникает вопрос: существует ли "санитарный" макрос для обнаружения злоупотребления макросом ARRAYSIZE
в C, желательно во время компиляции? В С++ мы просто использовали шаблон, специализированный только для аргументов массива; в C, кажется, нам нужно каким-то образом различать массивы и указатели. (Если бы я хотел отклонить массивы, например, я бы просто сделал, например, (arr=arr, ...)
, потому что назначение массива является незаконным).
Ответы
Ответ 1
Ядро Linux использует хорошую реализацию ARRAY_SIZE
для решения этой проблемы:
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]) + __must_be_array(arr))
с
#define __must_be_array(a) BUILD_BUG_ON_ZERO(__same_type((a), &(a)[0]))
и
#define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b))
Конечно, это переносимо только в GNU C, так как оно использует два instrinsics: typeof
и __builtin_types_compatible_p
. Также он использует свой "знаменитый" макрос BUILD_BUG_ON_ZERO
, который действителен только в GNU C.
Предполагая требование оценки времени компиляции (это то, что мы хотим), я не знаю никакой переносимой реализации этого макроса.
"Полупрозрачная" реализация (и которая не распространяется на все случаи):
#define ARRAY_SIZE(arr) \
(sizeof(arr) / sizeof((arr)[0]) + STATIC_EXP(IS_ARRAY(arr)))
с
#define IS_ARRAY(arr) ((void*)&(arr) == &(arr)[0])
#define STATIC_EXP(e) \
(0 * sizeof (struct { int ARRAY_SIZE_FAILED:(2 * (e) - 1);}))
С gcc
это не дает предупреждения, если аргумент представляет собой массив в -std=c99 -Wall
, но -pedantic
выдаст предупреждение. Причина в выражении IS_ARRAY
не является целочисленным постоянным выражением (приведение к типам указателей и оператору индекса не допускается в целых константных выражениях), а ширина битового поля в STATIC_EXP
требует целочисленного постоянного выражения.
Ответ 2
Эта версия ARRAYSIZE()
возвращает 0
, когда arr
является указателем и размером, когда его чистый массив
#include <stdio.h>
#define IS_INDEXABLE(arg) (sizeof(arg[0]))
#define IS_ARRAY(arg) (IS_INDEXABLE(arg) && (((void *) &arg) == ((void *) arg)))
#define ARRAYSIZE(arr) (IS_ARRAY(arr) ? (sizeof(arr) / sizeof(arr[0])) : 0)
int main(void)
{
int a[5];
int *b = a;
int n = 10;
int c[n]; /* a VLA */
printf("%zu\n", ARRAYSIZE(a));
printf("%zu\n", ARRAYSIZE(b));
printf("%zu\n", ARRAYSIZE(c));
return 0;
}
Вывод:
5
0
10
Как отметил Бен Джексон, вы можете заставить исключение во время выполнения (деление на 0)
#define IS_INDEXABLE(arg) (sizeof(arg[0]))
#define IS_ARRAY(arg) (IS_INDEXABLE(arg) && (((void *) &arg) == ((void *) arg)))
#define ARRAYSIZE(arr) (sizeof(arr) / (IS_ARRAY(arr) ? sizeof(arr[0]) : 0))
К сожалению, вы не можете принудительно выполнить ошибку времени компиляции (адрес arg
должен сравниваться во время выполнения)
Ответ 3
С C11 мы можем различать массивы и указатели, используя _Generic
, но я нашел способ сделать это, если вы укажете тип элемента:
#define ARRAY_SIZE(A, T) \
_Generic(&(A), \
T **: (void)0, \
default: _Generic(&(A)[0], T *: sizeof(A) / sizeof((A)[0])))
int a[2];
printf("%zu\n", ARRAY_SIZE(a, int));
Макрос проверяет: 1) указатель-на-не является указателем на указатель. 2) pointer-to-elem является указателем на T. Он оценивает значение (void)0
и статически статирует указатели.
Это несовершенный ответ, но, возможно, читатель может улучшить его и избавиться от этого параметра!
Ответ 4
Модификация ответа bluss с использованием typeof вместо параметра типа:
#define ARRAY_SIZE(A) \
_Generic(&(A), \
typeof((A)[0]) **: (void)0, \
default: sizeof(A) / sizeof((A)[0]))
Ответ 5
Здесь еще один, который опирается на gcc typeof extension:
#define ARRAYSIZE(arr) ({typeof (arr) arr ## _is_a_pointer __attribute__((unused)) = {}; \
sizeof(arr) / sizeof(arr[0]);})
Это работает, пытаясь настроить идентичный объект и инициализировать его назначенным массивом инициализатором. Если массив передан, компилятор счастлив. Если указатель передан, компилятор жалуется:
arraysize.c: In function 'main':
arraysize.c:11: error: array index in non-array initializer
arraysize.c:11: error: (near initialization for 'p_is_a_pointer')
Ответ 6
Здесь одно возможное решение с использованием расширения GNU, называемое выражением выражения :
#define ARRAYSIZE(arr) \
({typedef char ARRAYSIZE_CANT_BE_USED_ON_POINTERS[sizeof(arr) == sizeof(void*) ? -1 : 1]; \
sizeof(arr) / sizeof((arr)[0]);})
Это использует статическое утверждение, чтобы утверждать, что sizeof(arr) != sizeof(void*)
. У этого есть очевидное ограничение - вы не можете использовать этот макрос на массивах, размер которых является точно одним указателем (например, массив длиной 1 строки указателей/целых чисел или, возможно, массив длиной 4 длины в 32-битном Платформа). Но эти конкретные примеры могут быть проделаны достаточно легко.
Это решение не переносится на платформы, которые не поддерживают это расширение GNU. В таких случаях я бы рекомендовал использовать стандартный макрос и не беспокоиться о случайном прохождении указателей к макросу.
Ответ 7
Ужасно, да, но это работает и переносимо.
#define ARRAYSIZE(arr) ((sizeof(arr) != sizeof(&arr[0])) ? \
(sizeof(arr)/sizeof(*arr)) : \
-1+0*fprintf(stderr, "\n\n** pointer in ARRAYSIZE at line %d !! **\n\n", __LINE__))
Это не обнаружит ничего во время компиляции, но выведет сообщение об ошибке в stderr
и вернет -1
, если это указатель или длина массива равна 1.
== > DEMO < ==
Ответ 8
мой личный фаворит, пробовал gcc 4.6.3 и 4.9.2:
#define STR_(tokens) # tokens
#define ARRAY_SIZE(array) \
({ \
_Static_assert \
( \
! __builtin_types_compatible_p(typeof(array), typeof(& array[0])), \
"ARRAY_SIZE: " STR_(array) " [expanded from: " # array "] is not an array" \
); \
sizeof(array) / sizeof((array)[0]); \
})
/*
* example
*/
#define not_an_array ((char const *) "not an array")
int main () {
return ARRAY_SIZE(not_an_array);
}
компилятор печатает
x.c:16:12: error: static assertion failed: "ARRAY_SIZE: ((char const *) \"not an array\") [expanded from: not_an_array] is not an array"
Ответ 9
Еще один пример коллекции.
#define LENGTHOF(X) ({ \
const size_t length = (sizeof X / (sizeof X[0] ?: 1)); \
typeof(X[0]) (*should_be_an_array)[length] = &X; \
length; })
Плюсы:
- Он работает с обычными массивами, массивами переменной длины, многомерными
массивы, массивы нулевых размеров
- Он генерирует ошибку компиляции (не предупреждение), если вы передаете любой указатель, структуру или
объединение
- Это не зависит от каких-либо функций C11.
- Это дает вам очень читаемую ошибку.
Минусы: