Повышение при оценке константных целочисленных выражений в директивах препроцессора - 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:

  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)

Семантические правила для оценки постоянного выражения такие же, как и для неконстантных выражений.

Таким образом, правила оценки одинаковы по всем направлениям. В частности, одни и те же "обычные арифметические преобразования" применяются в каждом случае, когда типы операндов двоичного оператора различаются. Другие ответы говорят об этих деталях.

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