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

Скажем, у меня есть сценарий, где мне нужно убедиться, что значение, используемое в моем коде, является константой времени компиляции (например, возможно, драконовская интерпретация P10 правило 2 "фиксированные ограничения цикла" ). Как я могу обеспечить это на уровне языка в 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). Это также может быть отрицательным.

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

  • Случай:, выражение аргумента в строку 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 массивы указателям или нет.)

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

licensed under cc by-sa 3.0 with attribution.