Символ и обычные правила арифметического преобразования
Я знаю, что на этот вопрос задавали и, казалось бы, отвечали тысячи раз, но я не могу сопоставить ответы с моим собственным опытом.
Стандарт 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
, что точно так же.