In C++, unsigned integer types have well-defined overflow behavior when performing arithmetic in the form of wrapping, while signed integer types do not. Overflow behavior is also explicitly undefined and up to the compiler implementation.
Is there a platform-independent way to perform the various arithmetic operations to guarantee wrapping behavior for signed integer types? Ideally in a way that doesn't require reimplementing the arithmetic operations by hand.
Since C++20, signed integers are required to have a two's complement value representation. The special thing about two's complement is that for addition, subtraction, and multiplication, signed
and unsigned
operations are equivalent at the bit level. This phenomenon emerges from modular arithmetic.
For example, if you have a 4-bit integer, then:
// unsigned perspective
2 * 9 == 18 == 2
0b0010 * 0b1001 == 0b1'0010 == 0b0010
// signed perspective
2 * -7 == -14 == 2
0b0010 * 0b1001 == 0b1'0010 == 0b0010
Since the wrapping behavior of unsigned integers and two's complement integers is bit-equivalent, you can cast to unsigned
, perform the (well-defined) operation, and cast back:
int wrapping_multiply(int x, int y) {
return int(unsigned(x) * unsigned(y));
}
Or generically:
template <std::integral Int>
Int wrapping_multiply(Int x, Int y) {
using Uint = std::make_unsigned_t<Int>;
return Int(Uint(x) * Uint(y));
}
You could also define a class template, which would improve ergonomics, and you would no longer have to call functions like wrapping_multiply
by hand.
// class template with wrapping arithmetic operator overloads
template <std::integral Int>
struct wrapping_integer;
// some convenience aliases
using wrapping_int = wrapping_integer<int>;
using wrapping_long = wrapping_integer<long>;
Prior to C++20, this solution has a slight portability problem: the conversion from unsigned
to signed
types is implementation-defined if the value isn't preserved (i.e. when producing a negative number).
However, you can static_assert
that you get the expected result, as if two's complement was used.
This will already cover 99.9999% of all devices your code will ever compile on.
If you're worried about the remainder, you can manually define a conversion from unsigned
to signed
types in some function, which behaves as if two's complement was used. Otherwise, the behavior is implementation-defined.