Как я могу проверить, что выражение является константой в C?

Скажем, у меня есть сценарий, в котором мне нужно убедиться, что значение, используемое в моем коде, является константой времени компиляции (например, возможно драконовская интерпретация правила 2 P10 "фиксированные границы цикла"). Как я могу применить это на уровне языка в C?

C поддерживает понятие целочисленного константного выражения на уровне языка. Должна быть возможность выработать способ воспользоваться этим, чтобы в выражениях могли использоваться только значения, соответствующие этой спецификации, верно? например:

for (int i = 0; i < assert_constant(10); ++i) {...

Некоторые частичные решения, которые не являются достаточно общими, чтобы быть полезными в нескольких ситуациях:

  • Битовые поля: классическая стратегия реализации static_assert в C до C11 использовалось битовое поле, значение которого было бы недопустимым в случае сбоя условия:

    struct { int _:(expression); }
    

    Хотя это можно легко обернуть для использования в качестве части выражения, оно вообще не является общим - максимальное значение expression "[не может превышать ширину объекта указанного типа, если бы двоеточие и выражение отсутствовали" (C11 6.7.2.1), что накладывает очень низкий переносимый предел на величину expression (как правило, может быть 64). Это также не может быть отрицательным.

  • Перечисления: enum требует, чтобы любые инициализирующие выражения были целочисленными константными выражениями. Тем не менее, enum объявление не может быть встроено в выражение (в отличие от struct определение), требуя своего собственного заявления. Поскольку идентификаторы в списке перечислителя добавляются в окружающую область, нам также каждый раз требуется новое имя. __COUNTER__ не стандартизирован, поэтому нет способа достичь этого из макроса.

  • Case: опять же, выражение аргумента для case Строка должна быть целочисленной константой. Но это требует окружения switch заявление. Это не намного лучше, чем enumи это та вещь, которую вы не хотите скрывать внутри макроса (поскольку он будет генерировать реальные операторы, даже если их легко удалить оптимизатору).

  • Объявление массива: начиная с C99, размер массива даже не должен быть константой, то есть в любом случае он не будет генерировать нужную ошибку. Это также утверждение, которое требует введения имени в окружающую область, страдающую от тех же проблем, что и enum,

Конечно, есть какой-то способ скрыть постоянную проверку в макросе, который повторяется, передает значение (так что его можно использовать как выражение) и не требует строки оператора или введения дополнительных идентификаторов?

1 ответ

Решение

Оказывается, есть способ!

Хотя локально распределенным массивам разрешено иметь переменную длину в C, стандарт явно требует, чтобы такие массивы не имели явного инициализатора. Мы можем принудительно отключить функцию языка VLA, предоставив массиву список инициализатора, который заставит размер массива быть выражением целочисленной константы (константа времени компиляции):

int arr[(expression)] = { 0 };

Содержание инициализатора не имеет значения; { 0 } всегда будет работать

Это все еще немного уступает enum решение, потому что оно требует утверждения и вводит имя. Но, в отличие от перечислений, массивы можно сделать анонимными (как составные литералы):

(int[expression]){ 0 }

Так как составной литерал имеет инициализатор как часть синтаксиса, для него никогда не будет VLA, так что это все еще гарантированно потребует expression быть константой времени компиляции.

Наконец, поскольку анонимные массивы являются выражениями, мы можем передать их sizeof что дает нам возможность преодолеть первоначальную ценность expression:

sizeof((char[expression]){ 0 })

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

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

sizeof((char[(expression)*0+1]){ 0 }) * 0 + (expression)

Это не учитывает фактическую стоимость expression при установке размера массива (который всегда будет 1), но все равно учитывает его постоянный статус; затем он также игнорирует размер массива и возвращает только исходное выражение, поэтому ограничения на размер массива - должны быть больше нуля - не нужно применять к возвращенному значению. expression дублируется, но это то, для чего нужны макросы (и если он компилируется, он не будет пересчитан, потому что a. это константа, и b. первое использование находится в пределах sizeof). Таким образом:

#define assert_constant(X) (sizeof((char[(X)*0+1]){ 0 }) * 0 + (X))

Для получения бонусных баллов мы можем использовать очень похожую технику для static_switch выражение, объединяя размеры массива с C11 _Generic (это, вероятно, не имеет большого практического применения, но может заменить некоторые случаи вложенных тернаров, которые не популярны):

#define static_switch(VAL, ...) _Generic(&(char[(VAL) + 1]){0}, __VA_ARGS__)
#define static_case(N) char(*)[(N) + 1]

char * x = static_switch(3,
             static_case(0): "zero",
             static_case(1): "one",
             static_case(2): "two",
             default: "lots");
printf("result: '%s'\n", x); //result: 'lots'

(Мы берем адрес массива для создания явного типа указатель на массив, а не позволяем реализации решать, _Generic продвигает массивы к указателям или нет; по состоянию на апрель 2016 г. эта двусмысленность была исправлена ​​в языке DR 481 и в последующем TC.)

Это немного более ограничительно, чем assert_constant потому что он не допустит отрицательных значений. Положив +1 однако как в управляющем выражении, так и во всех значениях регистра мы можем, по крайней мере, позволить ему принять ноль.

Другие вопросы по тегам