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>;
Notes on Pre-C++20
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.