Почему знак отличается после вычитания неподписанных и подписанных?

unsigned int t = 10;
int d = 16;
float c = t - d;
int e = t - d;

Почему значение c положительным, но e отрицательным?

Ответ 1

Начнем с анализа результата t - d.

t является unsigned int а d является int, поэтому для выполнения арифметики на них значение d преобразуется в unsigned int (C++ правила говорят, что unsigned получает предпочтение здесь). Таким образом, мы получаем 10u - 16u, который (предполагая 32-битный int) обтекает 4294967290u.

Затем это значение преобразуется в float в первом объявлении и в int во втором.

Предполагая, что типичная реализация float (32-битная 1e38 IEEE), ее наивысшее представимое значение составляет примерно 1e38, поэтому 4294967290u находится в пределах этого диапазона. Будут ошибки округления, но преобразование в float не будет переполняться.

Для int ситуация другая. 4294967290u слишком большой, чтобы вписаться в int, поэтому происходит обход, и мы возвращаемся к значению -6. Обратите внимание, что такой обертку не гарантируется стандартом: результирующее значение в этом случае определяется реализацией (1) что означает, что для компилятора это значение результата, но оно должно быть документировано.


(1) C++ 17 (N4659), [conv.integral] 7.8/3:

Если тип назначения подписан, значение не изменяется, если оно может быть представлено в типе назначения; в противном случае значение определяется реализацией.

Ответ 2

Во-первых, вы должны понимать "обычные арифметические преобразования" (эта ссылка для C, но правила в C++ одинаковы). В C++, если вы выполняете арифметику со смешанными типами (вам следует избегать, если это возможно, кстати), существует набор правил, которые определяют, в каком типе выполняется вычисление.

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

Таким образом, ваш расчет равен 10 - 16 в unsigned int арифметике. Беззнаковая арифметика является по модулю арифметикой, что означает, что она обертывается. Итак, предполагая ваш типичный 32-битный int, результат этого расчета составляет 2 ^ 32 - 6.

Это одинаково для обеих линий. Обратите внимание, что вычитание полностью не зависит от назначения; тип с левой стороны абсолютно не влияет на то, как происходит расчет. Общей ошибкой начинающих считается мысль о том, что тип на левой стороне каким-то образом влияет на расчет; но float f = 5/6 равно нулю, потому что деление по-прежнему использует целочисленную арифметику.

Следовательно, разница заключается в том, что происходит во время назначения. Результат вычитания неявно преобразуется в float в одном случае, а int в другой.

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

Преобразование в int говорит, что если значение вписывается в диапазон int, значение не изменится. Но 2 ^ 32 - 6 намного больше, чем 2 ^ 31 - 1, что может содержать 32-битный int, поэтому вы получаете другую часть правила преобразования, в которой говорится, что результирующее значение определяется реализацией. Это термин в стандарте, который означает, что "разные компиляторы могут делать разные вещи, но им приходится документировать, что они делают".

Для всех практических целей все компиляторы, с которыми вы, вероятно, столкнетесь, скажете, что бит-шаблон остается неизменным и просто интерпретируется как подписанный. Из-за того, как работает арифметика с двумя дополнениями (так, что почти все компьютеры представляют отрицательные числа), результатом является -6, который вы ожидаете от вычисления.

Но все это очень длинный путь повторения первой точки, которая "не выполняет смешанную арифметику типа". Сначала введите типы, явно, в типы, которые, как вы знаете, сделают правильные вещи.