Перед арифметикой операнды меняют тип по правилам языка, а не по типу переменной, в которую пишется результат. Сначала малые типы продвигаются (integral promotion), затем оба операнда приводятся к общему типу (usual arithmetic conversions).
Зачем нужно
Процессор выполняет операцию над двумя значениями одного типа, и язык должен
однозначно сказать, какой это тип. От ответа зависит и результат, и сам факт
корректности программы: x + y для двух int по два миллиарда — это
Неопределённое поведение, а те же операнды в long long дают честный ответ.
Главное следствие: тип выражения не зависит от того, куда вы его присваиваете.
int x = 2'000'000'000;
int y = 2'000'000'000;
long long result = x + y; // всё ещё UB: сложение идёт в intШаг 1: продвижение малых типов
bool, char и short не участвуют в арифметике напрямую — их значения
сначала продвигаются до int, если int представляет все значения исходного
типа, иначе до unsigned int. false становится 0, true — 1.
short x = 1;
short y = 2;
std::cout << sizeof(x) << '\n'; // 2
std::cout << sizeof(x + y) << '\n'; // 4Переменные не выросли — вырос тип выражения. sizeof не вычисляет сумму,
а исследует её тип, поэтому им удобно наблюдать продвижение. Но размер не
восстанавливает тип однозначно: разные типы бывают одинаковой ширины.
Объяснять продвижение фразой «процессор не умеет складывать короткие числа» не нужно. Это правило языка, и из разрядности процессора оно не выводится.
Шаг 2: общий тип
После продвижения операнды приводятся к общему типу.
Оба знаковые или оба беззнаковые — берётся тип большего ранга. Порядок
рангов: int, long, long long.
0 + 0L // long
0LL + 0 // long long
0L + 0LL // long long
0U + 0 // unsigned int
0 + 0UL // unsigned longЗнаковый с беззнаковым — правило «всегда побеждает беззнаковый» неверно. Работает такая цепочка:
- если ранг беззнакового не меньше ранга знакового — выбирается беззнаковый;
- иначе, если знаковый тип вмещает весь диапазон беззнакового — выбирается знаковый;
- иначе — беззнаковый вариант знакового типа большего ранга.
Поэтому тип 0UL + 0LL зависит от реализации: при 32-битном unsigned long
и 64-битном long long это long long, а если оба 64-битные — уже
unsigned long long, потому что знаковый тип не вмещает все значения
беззнакового.
С плавающей точкой — порядок float, double, long double, второй
операнд приводится к старшему присутствующему дробному типу.
1.0F + 2 // float
1.0 + 2 // double
1.0 + 2.0L // long doubleНе любая операция с дробным числом выполняется в double.
Где это видно в задачах
Самая частая ловушка — сравнение знакового с беззнаковым:
std::cout << (-1 < 1U) << '\n'; // falseint приводится к unsigned int, −1 превращается в 4 294 967 295, и оно
не меньше единицы. Сравнение отработало правильно — неверным было ожидание,
что оно сравнит исходные математические числа. Подробнее — Беззнаковые типы.
Второй сюжет — промежуточное переполнение. Чинится не типом переменной слева, а типом операнда:
long long ok = static_cast<long long>(x) + y;
long long ok2 = 0LL + x + y;
long long bad = x + y + 0LL; // UB: x + y вычислено раньшеСм. static_cast.
Тонкости
- Схема из этой заметки описывает обычную арифметику. Не все операции ей следуют: у сдвигов свои правила определения типа результата — см. Побитовые операции.
- Продвижение применяется и к беззнаковым малым типам:
unsigned shortна обычной системе продвигается до знаковогоint, потому чтоintвмещает все его значения. Арифметика по модулю там уже не работает. sizeof(x + y)не вычисляетx + y— операндsizeofне выполняется. Даже если внутри написать выражение с переполнением, UB не будет.
Частые ошибки
- Ожидать, что широкий тип слева от
=расширит вычисление справа. - Считать правило «беззнаковый побеждает знаковый» универсальным.
- Складывать счётчики разной знаковости и удивляться сравнению.
- Думать, что
short + shortдаётshort.