Массив размера массива, который отклоняет указатели

Обычный макет размера массива, который часто изучается,

#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.
  • Это дает вам очень читаемую ошибку.

Минусы:

  • Это зависит от некоторых расширений gcc: Typeof, Statement Exprs и (если вам нравится) Conditionals
  • Это зависит от функции C99 VLA