Как работают правила продвижения, когда подпись с обеих сторон двоичного оператора различна?
Рассмотрим следующие программы:
// http://ideone.com/4I0dT
#include <limits>
#include <iostream>
int main()
{
int max = std::numeric_limits<int>::max();
unsigned int one = 1;
unsigned int result = max + one;
std::cout << result;
}
а также
// http://ideone.com/UBuFZ
#include <limits>
#include <iostream>
int main()
{
unsigned int us = 42;
int neg = -43;
int result = us + neg;
std::cout << result;
}
Как оператор + "знает", какой тип возвращать? Общее правило состоит в том, чтобы преобразовать все аргументы в самый широкий тип, но здесь нет четкого "победителя" между int
а также unsigned int
, В первом случае unsigned int
должен быть выбран в результате operator+
потому что я получаю результат 2147483648
, Во втором случае нужно выбирать int
потому что я получаю результат -1
, Тем не менее, я не вижу в общем случае, как это разрешимо. Это неопределенное поведение, которое я вижу, или что-то еще?
3 ответа
Это четко указано в §5/9:
Многие бинарные операторы, которые ожидают операнды арифметического или перечислимого типа, вызывают преобразования и выдают типы результатов аналогичным образом. Цель состоит в том, чтобы получить общий тип, который также является типом результата. Этот шаблон называется обычными арифметическими преобразованиями, которые определяются следующим образом:
- Если один из операндов имеет тип
long double
другой должен быть преобразован вlong double
,- В противном случае, если любой из операндов
double
другой должен быть преобразован вdouble
,- В противном случае, если любой из операндов
float
другой должен быть преобразован вfloat
,- В противном случае интегральные продвижения должны выполняться для обоих операндов.
- Затем, если любой из операндов
unsigned long
другой должен быть преобразован вunsigned long
,- В противном случае, если один операнд
long int
и другиеunsigned int
тогда, еслиlong int
может представлять все значенияunsigned int
,unsigned int
должны быть преобразованы вlong int
; в противном случае оба операнда должны быть преобразованы вunsigned long int
,- В противном случае, если любой из операндов
long
другой должен быть преобразован вlong
,- В противном случае, если любой из операндов
unsigned
другой должен быть преобразован вunsigned
,[Примечание: в противном случае единственным оставшимся случаем является то, что оба операнда
int
]
В обоих ваших сценариях результат operator+
является unsigned
, Следовательно, второй сценарий эффективно:
int result = static_cast<int>(us + static_cast<unsigned>(neg));
Потому что в этом случае значение us + neg
не может быть представлен int
, значение result
определяется реализацией - §4.7/3:
Если тип назначения подписан, значение не изменяется, если оно может быть представлено в типе назначения (и ширине битового поля); в противном случае значение определяется реализацией.
До стандартизации C существовали различия между компиляторами - некоторые следовали правилам "сохранения значений", а другие - "сохранению знака". Сохранение знака означало, что если любой операнд был беззнаковым, результат был беззнаковым. Это было просто, но иногда давало довольно удивительные результаты (особенно, когда отрицательное число было преобразовано в беззнаковое).
С стандартизирован по более сложным правилам "сохранения стоимости". В соответствии с правилами сохранения значений повышение может зависеть от фактических диапазонов типов, поэтому вы можете получить разные результаты на разных компиляторах. Например, на большинстве компиляторов MS-DOS, int
такой же размер как short
а также long
отличается от любого. На многих современных системах int
такой же размер как long
, а также short
отличается от любого. С правилами сохранения значений это может привести к тому, что продвигаемый тип будет отличаться между ними.
Основная идея правил сохранения значений заключается в том, что он будет преобразован в больший тип со знаком, если он может представлять все значения меньшего типа. Например, 16-битный unsigned short
может быть повышен до 32-разрядного signed int
потому что каждое возможное значение unsigned short
может быть представлен в виде signed int
, Типы будут переведены в тип без знака тогда и только тогда, когда это необходимо для сохранения значений меньшего типа (например, если unsigned short
а также signed int
оба 16 бит, то signed int
не может представлять все возможные значения unsigned short
так unsigned short
будет повышен до unsigned int
).
Когда вы присваиваете результат как у вас, результат все равно будет преобразован в тип назначения, так что большая часть этого имеет сравнительно небольшую разницу - по крайней мере, в большинстве типичных случаев, когда он просто копирует биты в результат, и вам решать, интерпретировать ли это как подписанный или неподписанный.
Если вы не назначите результат, такой как в сравнении, все может стать довольно уродливым. Например:
unsigned int a = 5;
signed int b = -5;
if (a > b)
printf("Of course");
else
printf("What!");
Под знаком сохранения правил, b
будет повышен до неподписанного, и в процессе станет равным UINT_MAX - 4
так что "Что!" нога if
будет принято. С помощью правил сохранения значений вы также можете получить некоторые странные результаты, немного похожие на это, но 1) в основном на DOS-подобных системах, где int
такой же размер как short
и 2) как правило, в любом случае это сделать сложнее.
Он выбирает любой тип, в который вы помещаете свой результат, или, по крайней мере, cout учитывает этот тип во время вывода.
Я не помню точно, но я думаю, что компиляторы C++ генерируют один и тот же арифметический код для обоих, это только сравнение и вывод, которые заботятся о знаке.