Похоже, что оператор typeof
, вероятно, будет принят в следующем стандарте C, и я искал способ использовать его для создания макроса с использованием переносимого ISO-C, который может получить длину массива, переданного в или не скомпилируется, если в него передан указатель. Обычно общий выбор можно использовать, чтобы вызвать ошибку компилятора при использовании нежелательного типа, исключив его из списка общих ассоциаций, но в этом случае нам нужна ассоциация по умолчанию для работы с массивами любой длины, поэтому вместо этого я пытаюсь вызвать ошибку компилятора для универсальной ассоциации для типа, который нам не нужен. Вот пример того, как может выглядеть макрос:
#define ARRAY_SIZE(X) _Generic(&(X), \
typeof(&X[0]) *: sizeof(struct{_Static_assert(0, "Trying to get the array length of a pointer"); int _a;}), \
default: (sizeof(X) / sizeof(X[0])) \
)
Проблема в том, что _Static_assert
срабатывает, даже если выбранная общая ассоциация является ассоциацией по умолчанию. Для простоты, поскольку рассматриваемая проблема не связана ни с чем, представленным в C23, мы создадим тестовую программу, которая явно работает, чтобы отклонить указатель на int:
#include <stdio.h>
#include <stdlib.h>
#define ARRAY_SIZE(X) _Generic(&(X), \
int **: sizeof(struct{_Static_assert(0, "Trying to get the array length of a pointer"); int _a;}), \
default: (sizeof(X) / sizeof(X[0])) \
)
int main(void) {
int x[100] = {0};
int *y = x;
int (*z)[100] = {&x};
printf("length of x: %zu\n", ARRAY_SIZE(x));
printf("length of y: %zu\n", ARRAY_SIZE(y));
printf("length of z: %zu\n", ARRAY_SIZE(z));
printf("length of *z: %zu\n", ARRAY_SIZE(*z));
return EXIT_SUCCESS;
}
Создавая вышеизложенное с помощью -std=c11
, я обнаружил, что _Static_assert
срабатывает на всех расширениях ARRAY_SIZE
, хотя я ожидал, что будут проблемы только с указателями, которые будут использовать общую ассоциацию int **
.
Согласно 6.5.1.1 p3 стандарта C11 для общего выбора,
None of the expressions from any other generic association of the generic selection is evaluated
Является ли это ошибкой в gcc и clang, или есть что-то, что я пропустил в стандарте, что может привести к оценке времени компиляции этого _Static_assert
в неиспользуемой универсальной ассоциации?
Неважно, какой общий выбор оценивается.
Когда выражение, являющееся частью _Status_assert
, имеет значение 0, это считается нарушением ограничения, и компилятор должен сгенерировать диагностику.
Вы не можете смешивать _Static_assert
с выражениями, которые должны возвращать значение, такими как макрос, похожий на функцию. Возможно, вы могли бы обойти это с помощью «статического утверждения бедняка», как один из уродливых трюков, которые мы использовали до C11:
#define POOR_STATIC_ASSERT(expr) (int[expr]){0}
#define CHECK(X) _Generic((&X), \
int **: 0,\
default: (sizeof(X) / sizeof(X[0])) \
)
#define ARRAY_SIZE(X) ( (void)POOR_STATIC_ASSERT(CHECK(X)), CHECK(X) )
Здесь вызывается оператор запятой, чтобы макрос CHECK
возвращал размер или ноль, в зависимости от того, допустим тип или нет. Затем снова вызовите тот же макрос, чтобы вернуть его из функционального макроса ARRAY_SIZE
. Это приведет к некоторой загадочной ошибке от компилятора ISO C, такой как «ошибка: ISO C запрещает массив нулевого размера».
Следующая проблема заключается в том, что &(X)
в _Generic
ни в коем случае не сводится к int**
, поэтому этот макрос не является безопасным или надежным. Однако, что касается размеров массивов, мы можем использовать одну хитрость. Указатель на массив без размера (неполный тип) совместим с любым массивом элемента того же типа независимо от его размера. Макрос можно переписать так:
#define POOR_STATIC_ASSERT(expr) (int[expr]){0}
#define CHECK(X) _Generic((&X), \
int (*)[]: sizeof(X) / sizeof(X[0]), \
default: 0)
#define ARRAY_SIZE(X) ( (void)POOR_STATIC_ASSERT(CHECK(X)), CHECK(X) )
Это будет работать для любого массива int
независимо от размера, но не для всего остального.
@ChristianGibbons Думаю, вы могли бы использовать и отрицательные значения. Или просто скомпилируйте с помощью -std=c17 -pedantic-errors
, чтобы отключить расширения gcc.
Второе дополнение: можете ли вы подробнее рассказать о последнем сегменте, что он не обязательно сводится к int **
? В этом примере я пытался упростить проблему, чтобы иметь дело только с int
, но конечная цель — использовать C23 _Typeof (или любую другую нотацию, которую он использует). Тот факт, что можно использовать массив без размера, делает его немного спорным, но мне все еще любопытен сценарий, в котором случай int **
не работает (или, более того, typeof(&X[0]) *
)
Я также только что попытался заменить содержимое вашего POOR_STATIC_ASSERT
своим (sizeof(struct{ _Static_assert(expr, "Assert message"); int _a;}))
, и это действительно сработало. Таким образом, кажется, что ключом было переместить утверждение за пределы общего выбора, и теперь может быть создано правильное сообщение об ошибке компилятора.
После некоторого дальнейшего тестирования я обнаружил проблему. В моей версии, которая заменяет ваш POOR_STATIC_ASSERT
и использует _Static_assert
, _Static_assert не работает с массивом переменной длины, потому что выражение неизвестно во время компиляции. В вашей исходной версии это не удается с VLA, потому что sizeof
для VLA оценивается во время выполнения, поэтому создаваемый массив представляет собой VLA, который не может быть инициализирован.
Используя некоторые предложения из ответа Лундина, я придумал следующее решение упрощенной проблемы:
#define STATIC_ASSERT_EXPRESSION(X, ERROR_MESSAGE) (sizeof(struct {_Static_assert((X), ERROR_MESSAGE); int _a;}))
#define NO_POINTERS(X) _Generic(&(X), \
int (*)[]: 1, \
default: 0 \
)
#define ARRAY_SIZE(X) ( (void)STATIC_ASSERT_EXPRESSION(NO_POINTERS(X), "Cannot retrieve the number of array elements from a pointer"), (sizeof(X) / sizeof(X[0])) )
Чтобы фактическим вариантом использования был универсальный тип с использованием typeof, который должен соответствовать стандарту C23, замените макрос NO_POINTERS
следующим образом:
#define NO_POINTERS(X) _Generic(&(X), \
typeof(*X) (*)[]: 1, \
default: 0 \
)
Перемещая _Static_assert
за пределы общего выбора, он будет оцениваться только со значением, которое фактически возвращается из выбора, поэтому он не будет сброшен из-за наличия в неиспользованном выборе. Кроме того, вычисление количества элементов также было удалено из общего выбора, чтобы выражение общего выбора можно было безопасно использовать в статическом утверждении, даже если ваш массив был массивом переменной длины, который требует, чтобы его размер вычислялся во время выполнения. .
Сам Static Assert помещается внутри анонимной структуры, размер которой мы берем так, чтобы она была частью выражения. И затем, как в примере Лундина, мы используем оператор запятой для оценки этого выражения, а затем выбрасываем и используем результаты вычисления размера массива.
При этом мы отклоняем указатели, получая количество элементов как в статических массивах, так и в VLA, а также получаем хорошее сообщение об ошибке компилятора при попытке передать указатель.
Мне нравится такой подход. Я пытался найти другие способы форсирования ошибок компилятора, но все они заключались в том, чтобы поместить ошибку в один из общих вариантов выбора, а не пытаться использовать значение из выбора для форсирования ошибки. Пара дополнительных вопросов к этому ответу: есть ли причина не использовать отрицательное значение, а не
0
? Компилятор, который я тестировал (clang 5), разрешал массивы нулевой длины через расширение даже при компиляции со стандартом iso c11.