Повышение при оценке константных целочисленных выражений в директивах препроцессора - GCC
ПРИМЕЧАНИЕ. См. Мои правки ниже.
ОРИГИНАЛЬНЫЙ ВОПРОС:
Наткнулся на какое-то любопытное поведение, с которым я не могу смириться:
#if -5 < 0
#warning Good, -5 is less than 0.
#else
#error BAD, -5 is NOT less than 0.
#endif
#if -(5u) < 0
#warning Good, -(5u) is less than 0.
#else
#error BAD, -(5u) is less than 0.
#endif
#if -5 < 0u
#warning Good, -5 is less than 0u.
#else
#error BAD, -5 is less than 0u.
#endif
Когда скомпилировано:
$ gcc -Wall -o pp_test.elf pp_test.c
pp_test.c:2:6: warning: #warning Good, -5 is less than 0.
pp_test.c:10:6: error: #error BAD, -(5u) is less than 0.
pp_test.c:13:9: **warning: the left operand of "<" changes sign when promoted**
pp_test.c:16:6: error: #error BAD, -5 is less than 0u.
Это говорит о том, что препроцессор следует правилам продвижения различных типов при оценке константных целочисленных выражений. А именно, когда у оператора есть операнды со смешанным знаком, подписанный операнд заменяется на беззнаковый операнд. Противоположность (вообще) верна в C.
Я не могу найти ничего в литературе, чтобы поддержать это, но возможно (вероятно?), Что я не был достаточно тщательным. Я что-то пропустил? Это поведение правильно?
В его нынешнем виде кажется, что любое условное выражение в директиве #if или #elif, которое включает в себя целочисленную константу без знака, может не работать так, как ожидалось, то есть как в C.
РЕДАКТИРОВАТЬ: Согласно моим комментариям в ответе Сурава Гоша, моя путаница первоначально возникла из выражений, которые включали константы, указанные с L
а также LL
суффиксы. Пример кода, который я включил в свой оригинальный вопрос, был слишком упрощен. Вот лучший пример:
#if -5LL < 0L
#warning Good, -5LL is less than 0L.
#else
#error BAD, -5LL is NOT less than 0L.
#endif
#if -(5uLL) < 0L
#warning Good, -(5uLL) is less than 0L.
#else
#error BAD, -(5uLL) is less than 0L.
#endif
#if -5LL < 0uL
#warning Good, -5LL is less than 0uL.
#else
#error BAD, -5LL is less than 0uL.
#endif
Строительство:
$ gcc -Wall -o pp_test.elf pp_test.c
pp_test.c:2:6: warning: #warning Good, -5LL is less than 0L.
pp_test.c:10:6: error: #error BAD, -(5uLL) is less than 0L.
pp_test.c:13:9: warning: the left operand of "<" changes sign when promoted
pp_test.c:16:6: error: #error BAD, -5LL is less than 0uL.
Это, кажется, нарушает пункт в 6.3.1.8, следующий за тем, что опубликовал Сурав Гош (мой акцент):
В противном случае, если тип операнда с целочисленным типом со знаком может представлять все значения типа операнда с целочисленным типом без знака, тогда операнд с целочисленным типом без знака преобразуется в тип операнда с целочисленным типом со знаком.
Кажется, нарушать этот пункт, потому что -5LL
имеет ранг, который выше, чем 0uL
и потому что тип первый (signed long long
) действительно может представлять все значения типа второго (unsigned long
). Загвоздка в том, что препроцессор этого не знает.
Как указано в https://gcc.gnu.org/onlinedocs/gcc-3.0.2/cpp_4.html (мой акцент):
Препроцессор вычисляет значение выражения. Он выполняет все вычисления в самом широком целочисленном типе, известном компилятору; на большинстве машин, поддерживаемых GCC, это 64 бита. Это не то правило, которое компилятор использует для вычисления значения константного выражения, и в некоторых случаях может давать разные результаты. Если значение оказывается отличным от нуля, "#if" завершается успешно, и контролируемый текст включается; в противном случае оно пропускается.
Кажется, что подразумевается под " выполнением всех вычислений в самом широком целочисленном типе, известном компилятору ", - это то, что сами операнды обрабатываются так, как если бы они были определены как тот же самый "самый широкий" тип. Другими словами, -5
а также -5L
обрабатываются так, как будто они -5LL
, а также 0u
а также 0uL
обрабатываются так, как будто они 0uLL
, Это активирует пункт, цитируемый Суравом Гошем, и приводит к наблюдаемому поведению.
В сущности, для препроцессора существует только один ранг, поэтому правила продвижения типов, которые зависят от операндов с другим рангом, игнорируются. Разве это не отличается от того, как компилятор оценивает выражения?
РЕДАКТИРОВАНИЕ № 2: Вот реальный пример того, как препроцессор по-разному оценивает одно и то же выражение, чем компилятор (взят из Optiboot).
#ifndef BAUD_RATE
#if F_CPU >= 8000000L
#define BAUD_RATE 115200L
#elif F_CPU >= 1000000L
#define BAUD_RATE 9600L
#elif F_CPU >= 128000L
#define BAUD_RATE 4800L
#else
#define BAUD_RATE 1200L
#endif
#endif
#ifndef UART
#define UART 0
#endif
#define BAUD_SETTING (( (F_CPU + BAUD_RATE * 4L) / ((BAUD_RATE * 8L))) - 1 )
#define BAUD_ACTUAL (F_CPU/(8 * ((BAUD_SETTING)+1)))
#define BAUD_ERROR (( 100*(BAUD_ACTUAL - BAUD_RATE) ) / BAUD_RATE)
#if BAUD_ERROR >= 5
#error BAUD_RATE error greater than 5%
#elif (BAUD_ERROR + 5) <= 0
#error BAUD_RATE error greater than -5%
#elif BAUD_ERROR >= 2
#warning BAUD_RATE error greater than 2%
#elif (BAUD_ERROR + 2) <= 0
#warning BAUD_RATE error greater than -2%
#endif
volatile long long int baud_setting = BAUD_SETTING;
volatile long long int baud_actual = BAUD_ACTUAL;
volatile long long int baud_error = BAUD_ERROR;
void foo(void) {
baud_setting = BAUD_SETTING;
baud_actual = BAUD_ACTUAL;
baud_error = BAUD_ERROR;
}
Сборка для цели AVR:
$ avr-gcc -Wall -c -g -save-temps -o optiboot_pp_test.elf -DF_CPU=8000000L optiboot_pp_test.c
Обратите внимание, как F_CPU
был указан как константа со знаком.
optiboot_pp_test.c:28:6: warning: #warning BAUD_RATE error greater than -2% [-Wcpp]
#warning BAUD_RATE error greater than -2%
Это работает как ожидалось. Изучение объектного файла:
baud_setting = BAUD_SETTING;
8: 88 e0 ldi r24, 0x08 ; 8
a: 90 e0 ldi r25, 0x00 ; 0
c: a0 e0 ldi r26, 0x00 ; 0
e: b0 e0 ldi r27, 0x00 ; 0
10: 80 93 00 00 sts 0x0000, r24
14: 90 93 00 00 sts 0x0000, r25
18: a0 93 00 00 sts 0x0000, r26
1c: b0 93 00 00 sts 0x0000, r27
baud_actual = BAUD_ACTUAL;
20: 87 e0 ldi r24, 0x07 ; 7
22: 92 eb ldi r25, 0xB2 ; 178
24: a1 e0 ldi r26, 0x01 ; 1
26: b0 e0 ldi r27, 0x00 ; 0
28: 80 93 00 00 sts 0x0000, r24
2c: 90 93 00 00 sts 0x0000, r25
30: a0 93 00 00 sts 0x0000, r26
34: b0 93 00 00 sts 0x0000, r27
baud_error = BAUD_ERROR;
38: 8d ef ldi r24, 0xFD ; 253
3a: 9f ef ldi r25, 0xFF ; 255
3c: af ef ldi r26, 0xFF ; 255
3e: bf ef ldi r27, 0xFF ; 255
40: 80 93 00 00 sts 0x0000, r24
44: 90 93 00 00 sts 0x0000, r25
48: a0 93 00 00 sts 0x0000, r26
4c: b0 93 00 00 sts 0x0000, r27
... показывает, что ожидаемые значения назначены. А именно, baud_setting
получает 8
, baud_actual
получает 111111
, а также baud_error
получает -3
,
Теперь мы строим с F_CPU, определенным как беззнаковая константа (как обычно для этой цели):
$ avr-gcc -Wall -c -g -save-temps -o optiboot_pp_test.elf -DF_CPU=8000000UL optiboot_pp_test.c
optiboot_pp_test.c:22:6: error: #error BAUD_RATE error greater than 5%
#error BAUD_RATE error greater than 5%
Сообщенная ошибка имеет неправильную величину и неправильный знак.
Изучение объектного файла показывает, что он идентичен файлу, созданному со значением со знаком для F_CPU.
Теперь это не является неожиданностью, при том понимании, что препроцессор обрабатывает все константы как вариант со знаком или без знака самого широкого целочисленного типа.
Удивляет то, что это явно не упоминается ни в стандарте, ни в документах GCC (что я могу найти).
Да, правила C для оценки операндов строго следуют препроцессору, но только в том случае, если оба операнда двоичного оператора имеют одинаковый ранг. Я не могу найти текст в стандарте, который утверждает, что препроцессор обрабатывает все константы, указанные с или без L
или же LL
как будто они все были LL
до применения правил целочисленного продвижения, указанных в 6.3.1.8, я не могу найти упоминания об этом поведении в документации GCC. Наиболее близким является отрывок из документации GCC, приведенной выше, в которой говорится, что препроцессор "выполняет все вычисления в самом широком целочисленном типе, известном компилятору".
Это не означает (не должно) явно, что операнды обрабатываются так, как если бы они были указаны с суффиксами, обозначающими их как самый широкий целочисленный тип, известный компилятору. В самом деле, при отсутствии явного отрывка по этому вопросу я ожидал бы, что операнды будут подчиняться тем же правилам преобразования типов и целочисленного продвижения, которым подчиняются все операнды при оценке компилятором. Похоже, это не так. Подразумевается, что, исходя из вышеприведенных тестов, применение нормальных правил продвижения целых чисел C происходит после того, как препроцессор переводит операнды в самый широкий (знаковый или беззнаковый) целочисленный тип, известный компилятору.
Если кто-то может показать какой-либо явный и релевантный текст на эту тему, либо из стандарта, либо из документации GCC, мне интересно.
РЕДАКТИРОВАТЬ #3: примечание: я скопировал нижеследующие абзацы из раздела комментариев в само сообщение, так как было слишком много комментариев для его просмотра.
Если кто-то может показать какой-либо явный и релевантный текст на эту тему, либо из стандарта, либо из документации GCC, мне интересно.
Вот текст из 6.10.1:
- Для целей этого преобразования и оценки токена все целочисленные типы со знаком и все целочисленные типы без знака действуют так, как если бы они имели такое же представление, что и типы intmax_t и uintmax_t, определенные в заголовке < stdint.h >.
Это, казалось бы, зацепило это.
3 ответа
Процитирую обычное правило арифметического преобразования (выделение мое) из C11
стандарт, глава §6.3.1.8.
В противном случае, если операнд с целым типом без знака имеет ранг, больший или равный рангу типа другого операнда, тогда операнд с целым типом со знаком преобразуется в тип операнда с целым типом без знака.
Так же и ваш случай.
В общем, если вы попытаетесь выполнить какую-либо операцию, включающую как подписанный, так и неподписанный тип, оба операнда будут сначала переведены в неподписанный тип, а затем будет выполнена операция.
Читайте здесь о целочисленных преобразованиях для арифметических операций, включая сравнение.
Это в основном приводит к тому, что - для вашего примера, где вы смешиваете подписанный и неподписанный одинаковый ранг - подписанный преобразуется в представление без знака, а не наоборот. Итак, сравнение сделано без знака для последних двух. Это идентично для препроцессора и фактического компилятора.
6.3.1.3p2, для представления со знаком дополнения 2s (как в настоящее время наиболее распространено для стандартных процессоров) означает, что двоичное представление целочисленного значения со знаком просто повторно интерпретируется как беззнаковое (положительное) значение, поэтому сравнение не выполняется.
Обратите внимание, что вы должны включить -Wconversions
(gcc), чтобы увидеть предупреждения о таких проблемных конверсиях.
Интерпретация значений числовых констант препроцессором может отличаться от C в некоторых редких случаях как побочный эффект обработки всех целочисленных значений как самого широкого доступного типа со знаком или без знака, в зависимости от ситуации, независимо от спецификаторов ширины. Однако, учитывая полученные типизированные числовые значения, его правила для оценки условных выражений явно совпадают с C:
Полученные токены составляют выражение управляющей константы, которое оценивается в соответствии с правилами [Раздел] 6.6.
(C99, раздел 6.10.1)
В разделе 6.6 представлены правила C для константных выражений, среди которых (в пункте 11)
Семантические правила для оценки постоянного выражения такие же, как и для неконстантных выражений.
Таким образом, правила оценки одинаковы по всем направлениям. В частности, одни и те же "обычные арифметические преобразования" применяются в каждом случае, когда типы операндов двоичного оператора различаются. Другие ответы говорят об этих деталях.