Скажем, что вы используете <cstdint>
и типа типа std::uint8_t
и std::uint16_t
, и хотите сделать на них операции типа +=
и *=
. Вы хотите, чтобы арифметика на этих числах обертывалась модульно, как обычно в C/С++. Обычно это работает, и вы экспериментально работаете с std::uint8_t
, std::uint32_t
и std::uint64_t
, но не std::uint16_t
.
В частности, умножение с std::uint16_t
иногда неэффективно, с оптимизированными сборками, производящими все виды странных результатов. Причина? Undefined из-за целочисленного переполнения со знаком. Компилятор оптимизируется на основе предположения о том, что поведение Undefined не происходит, и поэтому начинает обрезать куски кода из вашей программы. Конкретное поведение Undefined заключается в следующем:
std::uint16_t x = UINT16_C(0xFFFF);
x *= x;
Причина в правилах продвижения на С++ и в том, что вы, как и почти все остальные в наши дни, используете платформу, на которой std::numeric_limits<int>::digits == 31
. То есть int
- это 32-разрядный (digits
счетчик бит, но не знаковый бит). x
получает повышение до signed int
, несмотря на то, что оно без знака, и 0xFFFF * 0xFFFF
переполнения для 32-разрядной арифметики со знаком.
Демонстрация общей проблемы:
// Compile on a recent version of clang and run it:
// clang++ -std=c++11 -O3 -Wall -fsanitize=undefined stdint16.cpp -o stdint16
#include <cinttypes>
#include <cstdint>
#include <cstdio>
int main()
{
std::uint8_t a = UINT8_MAX; a *= a; // OK
std::uint16_t b = UINT16_MAX; b *= b; // undefined!
std::uint32_t c = UINT32_MAX; c *= c; // OK
std::uint64_t d = UINT64_MAX; d *= d; // OK
std::printf("%02" PRIX8 " %04" PRIX16 " %08" PRIX32 " %016" PRIX64 "\n",
a, b, c, d);
return 0;
}
Вы получите хорошую ошибку:
main.cpp:11:55: runtime error: signed integer overflow: 65535 * 65535
cannot be represented in type 'int'
Как избежать этого, конечно, нужно бросить как минимум unsigned int
перед умножением. Только точный случай, когда число бит неподписанного типа точно равно половине числа бит int
, проблематично. Любое меньшее приведет к тому, что умножение не сможет переполняться, как при std::uint8_t
; любое большее приведет к тому, что тип будет точно сопоставлен с одним из ранжирования продвижения, как с std::uint64_t
совпадением unsigned long
или unsigned long long
в зависимости от платформы.
Но это действительно отстой: для этого требуется знать, какой тип проблематичен, исходя из размера int
на текущей платформе. Есть ли лучший способ, с помощью которого можно избежать Undefined поведения с беззнаковым целочисленным умножением без #if
mazes?