Let's say that you are using <cstdint>
and types like std::uint8_t
and std::uint16_t
, and want to do operations like +=
and *=
on them. You'd like arithmetic on these numbers to wrap around modularly, like typical in C/C++. This ordinarily works, and you find experimentally works with std::uint8_t
, std::uint32_t
and std::uint64_t
, but not std::uint16_t
.
Specifically, multiplication with std::uint16_t
sometimes fails spectacularly, with optimized builds producing all kinds of weird results. The reason? Undefined behavior due to signed integer overflow. The compiler is optimizing based upon the assumption that undefined behavior does not occur, and so starts pruning chunks of code from your program. The specific undefined behavior is the following:
std::uint16_t x = UINT16_C(0xFFFF);
x *= x;
The reason is C++'s promotion rules and the fact that you, like almost everyone else these days, are using a platform on which std::numeric_limits<int>::digits == 31
. That is, int
is 32-bit (digits
counts bits but not the sign bit). x
gets promoted to signed int
, despite being unsigned, and 0xFFFF * 0xFFFF
overflows for 32-bit signed arithmetic.
Demo of the general problem:
// 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;
}
You'll get a nice error:
main.cpp:11:55: runtime error: signed integer overflow: 65535 * 65535
cannot be represented in type 'int'
The way to avoid this, of course, is to cast to at least unsigned int
before multiplying. Only the exact case where the number of bits of the unsigned type exactly equals half the number of bits of int
is problematic. Any smaller would result in the multiplication being unable to overflow, as with std::uint8_t
; any larger would result in the type exactly mapping to one of the promotion ranks, as with std::uint64_t
matching unsigned long
or unsigned long long
depending on platform.
But this really sucks: it requires knowing which type is problematic based upon the size of int
on the current platform. Is there some better way by which undefined behavior with unsigned integer multiplication can be avoided without #if
mazes?
unsigned long long
" isn't a great idea? – Otisotitisint
, but not actually as large, would also be problematic (never seen an architecture on which such a type exists) – Tripx *= x;
promoteuint16_t
toint32_t
? I find some promotion rules in the Std, but can not map them to this, exactly. – Cretinismx = x * x;
The rules for multiplication say that the usual integral promotions are performed. – Tripuint8_t(a) * uint8_t(b)
does not do arithmetic on unsigned types, so that clause controlling unsigned arithmetic doesn't apply. Unexpected, but true. – Tripuint16_t
notunsigned short
. I agree that it is unexpected. But consideruint16_t a = 65535, b = 65534; int diff = b - a;
. Which result is more unexpected:diff == 65535
ordiff == -1
? – Tripshall
obey the laws of arithmetic modulo 2^n where n is the number of bits in the value representation of that particular size of integer, and furthermore that it implies unsigned arithmetic does not overflow. – Bigod1u*
type coercions all over the place. – Elevator