Символ и обычные правила арифметического преобразования

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

Стандарт C устанавливает, что для добавления "оба операнда должны иметь арифметический тип" (6.5.6.1). Типы Arithemitc охватывают целочисленные и плавающие типы (6.2.5.18), и, наконец, целочисленные типы - это char, short, int, long и long long, которые существуют как типы со знаком и без знака (6.2.5.4 и 6.2.5.6). Согласно правилам обычного арифметического преобразования: "Если оба операнда имеют одинаковый тип, дальнейшее преобразование не требуется". Все идет нормально.

Насколько я понял из примера из "Книги C", я понял, что "[n]o арифметика выполняется C с точностью короче, чем int", где применяется интегральное продвижение. Я не могу найти ссылку на это в стандарте, я, кажется, видел это много раз.

Поскольку unsigned char является арифметическим типом, а правила обычного арифметического преобразования гласят, что операнды одного и того же типа не нуждаются в преобразовании, зачем нужна целочисленное продвижение?

Я проверил это с помощью двух разных компиляторов. Я написал простую программу, которая делает сложение символов:

unsigned char a = 1;
unsigned char b = 2;
unsigned char c = a + b;

Целевая платформа - Atmel Mega8 uC, использующая 8-битную архитектуру. Следовательно, целочисленное сложение потребовало бы использования двух регистров, если операнды должны подвергаться интегральному продвижению.

Компиляция этого с использованием компилятора imagecraft avr без оптимизации и с включенными строгими параметрами ANSI C и переносимостью дает следующий код сборки:

mov R16, R20
add R16, R18

Используя avr-gcc (я не знаю о параметре ANSI, похожем на gcc -strict):

$ avr-gcc -O0 -mmcu=atmega8 -S -c main.c

Получившаяся сборка:

ldd r25,Y+1
ldd r24,Y+2
add r24,r25
std Y+3,r24

Результирующий код в обоих случаях работает с одним байтом. Я получаю аналогичные результаты для побитового | и & и логично || а также &&. Значит ли это, что стандарт разрешает арифметические операции над типами символов без интегрального продвижения или просто означает, что эти компиляторы не соответствуют стандарту?


Дополнительно:

Оказывается, все зависит от типа, в котором хранится результат. Приведенный выше пример верен только в том случае, если результат сохраняется в символе и не зависит от результата сложения. Установка a в 0xFF и b в 1 дает точно такой же код сборки.

Если тип c изменяется на unsigned int, результирующая сборка выглядит так:

mov R2,R20
clr R3
mov R16,R18 
clr R17
add R16,R2 
adc R17,R3 

Даже в том случае, когда результат может храниться в одном байте, то есть a=1 и b=2.

5 ответов

Решение

C 2011 (n1570) 6.3.1.8 ("Обычные арифметические преобразования") 1 гласит, что целочисленные продвижения выполняются до рассмотрения того, являются ли типы одинаковыми:

В противном случае целочисленные продвижения выполняются для обоих операндов. Затем к повышенным операндам применяются следующие правила:

Если оба операнда имеют одинаковый тип, дальнейшее преобразование не требуется...

Таким образом, в C абстрактной машине, unsigned char значения должны быть повышены до int перед выполнением арифметики. (Существует исключение для порочных машин, где unsigned char а также int имеют одинаковый размер. В этом случае, unsigned char значения повышаются до unsigned int скорее, чем int, Это эзотерика и не должна рассматриваться в нормальных ситуациях.)

В реальной машине операции должны выполняться так, чтобы получить те же результаты, как если бы они были выполнены в абстрактной машине. Поскольку имеют значение только результаты, фактические промежуточные операции не обязательно должны точно соответствовать абстрактной машине.

Когда сумма двух unsigned char значения присваиваются unsigned char объект, сумма преобразуется в unsigned char, Это преобразование по существу отбрасывает биты за пределы битов, которые вписываются в unsigned char,

Это означает, что реализация C получает тот же результат, делает ли это это:

  • Преобразовать значения в int,
  • Добавить значения с int арифметика.
  • Конвертировать результат в unsigned char,

или это:

  • Добавить значения с unsigned char арифметика.

Поскольку результат один и тот же, реализация C может использовать любой метод.

Для сравнения, мы можем рассмотреть это утверждение вместо: int c = a + b;, Также предположим, что компилятор не знает значений a а также b, В этом случае, используя unsigned char арифметика, чтобы сделать сложение может дать другой результат, чем преобразование значений в int и используя int арифметика. Например, если a 250 и b 200, то их сумма как unsigned char значения 194 (250 + 200 % 256), но их сумма в int арифметика составляет 450. Поскольку существует различие, реализация C должна использовать инструкции, которые получают правильную сумму, 450.

(Если компилятор знал значения a а также b или иным образом доказать, что сумма вписывается в unsigned charтогда компилятор может снова использовать unsigned char арифметика.)

Вот соответствующая часть из C99:

6.3.1 Арифметические операнды
6.3.1.1 Булевы, символы и целые числа
1 Каждый целочисленный тип имеет ранг целочисленного преобразования, определенный следующим образом:
...
2 Следующее может использоваться в выражении везде, где могут использоваться int или unsigned int:
- Объект или выражение с целочисленным типом, чей ранг целочисленного преобразования меньше, чем ранг int и unsigned int.
- Битовое поле типа _Bool, int, sign int или unsigned int.
Если int может представлять все значения исходного типа, значение преобразуется в int; в противном случае он конвертируется в беззнаковое целое. Они называются целочисленными акциями. Все остальные типы не изменяются целочисленными акциями.

Я согласен, что это неясно, но это самое близкое, что вы можете найти по отношению к различным видам char или же short или же _Bool в int или же unsigned int,

Из того же источника:

5.1.2.3 Выполнение программы
В абстрактной машине все выражения оцениваются в соответствии с семантикой. Реальная реализация не должна оценивать часть выражения, если она может сделать вывод, что ее значение не используется и что не возникает никаких побочных эффектов (включая любые, вызванные вызовом функции или доступом к изменчивому объекту).
...
10 Пример 2. При выполнении фрагмента
символ с1, с2;
/ *... * /
с1 = с1 + с2;
"целочисленные продвижения" требуют, чтобы абстрактная машина увеличивала значение каждой переменной до размера int, а затем добавляла два целых числа и усекала сумму. При условии, что добавление двух символов может быть выполнено без переполнения или с автоматическим переносом переполнения для получения правильного результата, фактическое выполнение должно давать только тот же результат, возможно, без продвижения.

В 6.3.1.8 (обычные арифметические преобразования, n1570) мы можем прочитать

В противном случае целочисленные продвижения выполняются для обоих операндов. Затем к повышенным операндам применяются следующие правила:

Таким образом, целочисленное продвижение является частью обычных арифметических преобразований для целочисленных типов.

Таким образом, в машине abstratc преобразование в (unsigned) int должно быть сделано.

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

Таким образом, если гарантируется, что вычисление с использованием только одного байта будет иметь тот же результат, что и вычисление, способствующее intВ реализации допускается использование однобайтовой арифметики.

Если компьютер способен выполнять операции с типами меньше intТогда стандарт никогда не будет этому препятствовать. Имейте в виду, что стандарт пытается сохранить как можно больше опций, доступных для компиляторов, и оставляет решение выбрать лучший метод для них.

Фраза "арифметика не выполняется C с точностью короче, чем int" также является правильной. Если вы обратите пристальное внимание, вы увидите, что арифметика действительно выполняется с точностью, не короче int, Это, однако, не означает, что компилятор вынужден выполнять целочисленное продвижение, поскольку он может безопасно выполнять операции в вашем примере программы на байтах и ​​получать ту же точность.

Я думаю, что здесь нет противоречия. Компилятор не обязан следовать какому-либо конкретному пути вычислений, пока наблюдаемый результат таков, как если бы он следовал предписанному пути.

В частности, для вашего случая, если бы мы делали вычисления с повышением до int (скажем, до 16 бит): a повышен до int имеет то же значение, так и делает b также. Значение a + b на самом деле (a + b) mod 2^16, но мы присваиваем это беззнаковому символу, который усекает старшие 8 битов, что аналогично получению результата mod 2^8: ((a + b) mod 2^16) mod 2^8 = (a + b) mod 2^8,

Расчет без целочисленного продвижения приведет к (a + b) mod 2^8, что точно так же.

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