Вот вам маленькая задачка на программирование: реализуйте такой макрос, который принимает в качестве аргумента числовое выражение (числа могут быть целыми или с плавающей точкой) и:
Удостоверяет, что выражение является константой (т.e, его значение известно во время компиляции), а если это не так — отменяет компиляцию.
«Возвращает» то же самое значение.
Дополнительно: пусть возвращаемое значение относится к тому же типу, что и исходное выражение.
Эту задачу можно решать разными способами, в зависимости от того, каким стандартом C вы пользуетесь, и разрешены ли в вашем случае расширения компилятора. Ниже перечислю некоторые варианты таких решений и расскажу об их достоинствах и недостатках.
Составные литералы constexpr
Если вы работаете со стандартом C23 или выше, то можете указать, как долго должны храниться составные литералы. Сочетая их с typeof
и constexpr, которые также стандартизированы в C23, можно поступить так:
#define C(x) ( (constexpr typeof(x)){x} )
В таком случае тип не затрагивается. При этом, поскольку инициализаторы длительности хранения constexpr
сами обязательно должны быть констатными выражениями, компилятор обеспечит, что x
— это константное выражение.
Недостатки:
Требуется поддержка C23, которая пока не очень распространена.
Clang (v20) по-видимому не позволяет задавать длительность хранения составных литералов (а в GCC v14.2 такая функция вроде бы работает).
Поскольку для указания длительности хранения static
необходимо инициализировать эту операцию константным выражением, компилятор об этом позаботится.
Правда, constexpr
чуть богаче семантически, чем его более ранний аналог static
. Дело в том, что constexpr
требует, чтобы результирующее выражение (а не только инициализатор) тоже было константным.
__builtin_constant_p
Если вы располагаете расширениями GNU, то можете воспользоваться __builtin_constant_p
, возвращающим true
, когда выражение является константным, а вместе с тем — и __builtin_choose_expr
для выбора.
__attribute((error("not constant"))) int notconst(void);
#define C(x) __builtin_choose_expr(__builtin_constant_p(x), (x), notconst())
Когда __builtin_constant_p
возвращает true
, __builtin_choose_expr
выбирает первое выражение. В противном случае он вызывает формальную несуществующую функцию с атрибутом error, тем самым провоцируя ошибку компиляции.
В отличие от тернарных операторов, __builtin_choose_expr
не подчиняется правилам продвижения типов, поэтому никак не сказывается на типе выражения.
Недостатки:
Для работы нужны расширения GNU.
static_assert
Этот фокус мне показал пользователь constxd в одном IRC-канале. Здесь мы используем static_assert
из C11+, показывая с его помощью, что выражение — статическое. Но как же «вернуть» выражение? Ну… для этого нужно немного поколдовать с sizeof +
анонимными struct
:
#define C(x) ((x) + 0*sizeof( \
struct { _Static_assert((int)(x) || 1, ""); char tmp; } \
))
Выглядит диковато. Как можно было впустить static_assert прямо внутрь определения структуры? Дело в том, что стандарт классифицирует static_assert как объявление, которое ничего не объявляет (не спрашивайте меня, почему). Поэтому синтаксически ничто не мешает нам просто поместить это в структуру.
Сложение 0*sizeof(...) в сущности, операцией не является, поэтому значение остаётся неизменным. Но тип может измениться в результате продвижения.
Недостатки:
В результате сложения тип выражения может измениться, это соответствует правилам типичного целочисленного продвижения. (Правда, проблема может решаться при помощи оператора-запятой ).
В стандарте сказано, что выражение
static_assert
должно быть целочисленным и константным, но, как кажется, gcc и clang в данном случае может принять и выражение с плавающей точкой; правда, при этом будут выдаваться предупреждения. (Замечание: при непосредственном приведенииint
получается работоспособная константа с плавающей точкой, напр., 1.1 но такие вещи как 1.1 + 2.2 без выдачи предупреждений не пройдут).
sizeof + составной литерал с типом массива
Этот фокус похож на описанный выше, но в данном случае вместо static_assert
используется составной литерал с типом массива. С его помощью обеспечивается, что выражение является константным:
#define C(x) ( (x) + 0*sizeof( (char [(int)(x) || 1]){0} ) )
Что здесь интересно отметить:
1. Со времён C99 массивы (и соответствующие им типы) могут иметь переменную длину. Но составные литералы не принимают типов переменной длины, что и послужит нам верификацией.
2. В стандартном C (к сожалению) запрещены массивы нулевой длины, и поэтому || 1
.
3. Если массив превысит определённый размер, то объявить его будет невозможно (даже если не будет выделяться никакого пространства под хранение данных). На моём 64-разрядном ПК gcc отказывается работать с размерами сверх PTRDIFF_MAX (263-1)
байт. Clang ещё консервативнее и отвергает всё, что больше 61 бита. Не считая проблем с поддержкой 0
, || 1
также ужимает размер массива до 1.
Единственное преимущество этого способа по сравнению с использованием static_assert
заключается в том, что здесь нам не требуется C11, и для работы достаточно C99. В остальном мы получаем здесь в наследство все проблемы, присущие static_assert
:
Тип может меняться.
Не поддерживаются выражения с плавающей точкой (и, в отличие от случая с
static_assert
, gcc отказывается полностью скомпилировать выражение с плавающей точкой).
sizeof + константное перечисление
Поскольку константные перечисления — это обязательно целочисленные константные выражения, ими вполне можно воспользоваться вместо составного литерала:
#define C(x) ( (x) + 0*sizeof( enum { tmp = (int)(x) } ) )
Но здесь есть одна вопиющая проблема: в отличие от составных литералов, константные перечисления «протекают». То есть, таким макросом можно будет воспользоваться только один раз. Можно попробовать конкатенацию до обработки, чтобы прикреплять к строке её номер (__LINE__
), но в таком случае вы не сможете повторно использовать этот макрос в той же строке.
Вот симпатичное (как кому…) решеньице, которое я придумал: будем определять перечисление внутри параметра функции, так, чтобы оно приобретало «область видимости»:
#define C(x) ( (x) + 0*sizeof(void (*)(enum { tmp = (int)(x) })) )
Работает. Но и gcc, и clang предупреждают нас о том, что это перечисление анонимное… пусть именно этого мы и добивались. Причём, с помощью #pragma
этого не подавить, ведь мы имеем дело с макросом — и, следовательно, предупреждение выдаётся именно в той точке, где вызывается макрос.
Практическая польза от такого решения невелика, но оно совместимо с C89, если для вас это важно. Недостатки:
Тип может меняться.
Не поддерживаются выражения с плавающей точкой.
При каждом вызове макроса и gcc, и clang предупреждают вас, что перечисление является анонимным.
Оператор запятая
Все макросы, в которых используется фокус (x) + 0*sizeof(...)
, потенциально могут пострадать из-за изменения типа. Это красиво и элегантно решается: выносим sizeof
в отдельное выражение и игнорируем его при помощи оператора-запятой:
#define C(x) (sizeof(...), (x))
Но проблема в том, что вы будете получать целый ворох предупреждений вроде «операнд выражения с запятой в левой части не работает», пусть именно этого мы и добивались.
АПДЕЙТ: оказывается, можно заглушить неиспользуемые предупреждения, если приводить результат sizeof
к void
(уф!).
Чудачества GCC
Поначалу я не пользовался атрибутом error в моём решении с __builtin_constant_p
, отдавая предпочтение массиву с отрицательным размером (а в случаях, когда массивы переменной длины запрещены, оборачивал его в структуру), чтобы спровоцировать ошибку:
#define C(x) ((__typeof__(x)) ((x) + 0*sizeof( \
struct { char tmp[__builtin_constant_p(x) ? 1 : -1]; } \
)))
Ожидаемо, Clang сигнализирует об ошибке, когда размер массива составляет -1. Но GCC это проглатывает и не утруждает вас ничем кроме предупреждения (глубокий выдох). Поэтому используем здесь атрибут error
(Арсен, спасибо за подсказку), который должен хорошо страховать нас от таких странностей.
Заключение
Эта кроличья нора оказалась гораздо глубже, чем я изначально полагал. Эти назойливые предупреждения, конечно, раздражают. Но, поскольку я собирался использовать этот макрос в составе библиотеки, меня не устраивал вариант просто отключить предупреждение. Лучше я вообще уберу из библиотеки все предупреждения, чем обяжу моих пользователей самостоятельно их отключать.
Хотелось бы знать, есть ли какие-то (надеюсь, более выигрышные) решения, которые я упустил.
Обновление: u/P-p-H-d подсказал такое решение, в котором при помощи тернарного оператора _Generic
и правил обращения с константными нулевыми указателями удаётся определить, является ли константой выражение с целыми числами. Решение требует C11 и работает только с целыми числами, а к числам с плавающей точкой, к сожалению, неприменимо.
Бывает, что на языке C удобно различать, является ли данное выражение «постоянным выражением целочисленного типа» или «константой нулевого указателя». Например, это принципиально, если объект выделяется статически. Обычно это удаётся определить напрямую, уже когда мы пишем инициализатор. Но, если требуется инициализировать сравнительно сложную структуру такой функцией как инициализирующий макрос, в более ранних версиях C мы оказываемся перед выбором:
Воспользоваться расширением компилятора, например,
__builtin_constant_p
в gccНаписать две разные версии такого макроса — одну для статического выделения и одну для автоматического.
Далее объяснено, как этого добиться при помощи возможности _Generic
, предусмотренной в C11. Не знаю, предусмотрена ли аналогичная возможность в C++. Кроме того, в данном случае используется тернарный оператор (а в C и C++ они заметно отличаются), поэтому читатель должен иметь в виду, с каким из этих языков собирается работать.
Имея подобный формальный тип:
typedef struct p00_nullptr_test p00_nullptr_test;
получим, что выражение наподобие следующего во время компиляции результирует в константное значение 0 или 1, в зависимости от того, является ли Y константой нулевого указателя:
_Generic((1 ? (p00_nullptr_test*)0 : (void*)(Y)),
p00_nullptr_test*: true,
default: false)
Или результирует в myFunc(Y)
тогда и только тогда, когда Y является постоянным выражением целочисленного типа:
_Generic((1 ? (p00_nullptr_test*)0 : (void*)((Y)-(Y))),
p00_nullptr_test*: 0,
default: myFunc(Y))
Чтобы разобраться, как всё это работает, сначала нужно сделать небольшое отступление и выяснить, как устроен вышеупомянутый тернарный оператор. Считайте, что далее я подготовил вам небольшое упражнение, чтобы проверить ваши навыки C.
(1 ? (p00_nullptr_test*)X : (void*)Y)
Естественно, значение выражения равно X, но каков его тип?
Пожалуй, вы догадались: зависит от контекста, точнее, зависит от Y, но не от X. Но ещё интереснее, что зависит оно не только от типа Y (после приведения он всё равно будет неактуален), но и от значения. Если Y — это константа нулевого указателя, и только в этом случае, возвращаемый тип тернарного выражения будет p00_nullptr_test*
, в противном случае — void*
.
(void*)Y
может как являться, так и не являться константой нулевого указателя в том смысле, который вкладывается в этот термин в стандарте C. Если Y — это целочисленная константа, равная 0, то да, является, а если Y не целочисленная, не относится к типу void*, или её значение не равно 0, то это не константа нулевого указателя.
Тогда в первом случае допускаются целочисленный литерал (напр., 0, 0x0, '\0'), перечислимая константа со значением 0 (enum { zero }) или константное выражение, состоящее из литералов, перечислимых констант и выражений sizeof с условием результирования в 0.
Если Y примет любое из следующих значений, то в результате мы не получим константу нулевого указателя:
1 (значение не равно 0)
a (для любой переменной a, равной 0, даже квалифицируемой как константа)
(int*)0
(поскольку указатель после преобразования не являетсяvoid*
)(void const*)0
(по той же причине, квалифицированные версииvoid
не допускаются)
Теперь вернёмся к рассмотренному выше первому типу обобщённого выражения:
_Generic((1 ? (p00_nullptr_test*)0 : (void*)(Y)),
p00_nullptr_test*: true,
default: false)
и посмотрим, как оно работает. Как уже было сказано выше, если Y — это константа нулевого указателя, то тернарное выражение имеет тип p00_nullptr_test*
, и срабатывает первый вариант. Во всех остальных случаях тернарное выражение имеет тип void*
, и выбирается вариант, заданный по умолчанию.
При работе с обобщённым выражением второго типа действует другая логика. Известно, что любое арифметическое выражение вида (Y)-(Y)
всегда равно 0, но константой нулевого указателя оно будет только в том случае, когда Y — это константное выражение целочисленного типа.
Для P99 можно обернуть некоторые из этих вещей в макросы, готовые к использованию:
P99_GENERIC_INTEGRAL_CONSTANT(EXP, XTRUE, XFALSE) // является ли EXP целочисленной конконстантой во время компиляции
P99_GENERIC_NULLPTR_CONSTANT(PEXP, XTRUE, XFALSE) // является ли PEXP 0 или (void*)0 во время компиляции
P99_GENERIC_PCONST(PEXP, NCONST, CONST) // квалифицируется ли *PEXP как константа
В качестве 2-го и 3-го аргумента все они могут принимать произвольные (но валидные) выражения.
Например, вторым макросом можно воспользоваться для инициализации типа, являющегося указателем.
#define TRUC_INITIALIZER(PEXP) { \
.someField = 32, \
.pointerField = P99_GENERIC_NULLPTR_CONSTANT(PEXP, (truc*)0, truc_account(PEXP)),\
}
static truc the_static_truc = TRUC_INITIALIZER(0);
auto truc the_auto_truc = TRUC_INITIALIZER(42);
Здесь после инициализации переменной как static
она будет видеть только константные выражения. В свою очередь, при инициализации как auto
функция truc_account()
будет вызываться всякий раз, когда выполнение дойдёт до этой точки инициализации.