2

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.

cppguy
  • 3,611
  • 2
  • 21
  • 36
  • 1
    One way would be to cast to the corresponding unsigned type, perform the arithmetic operation, then cast back. – user3840170 Aug 14 '23 at 16:17
  • @user3840170 That only works for addition and subtraction, I think? – HolyBlackCat Aug 14 '23 at 16:17
  • Multiplication too. And division can be handled separately. – user3840170 Aug 14 '23 at 16:28
  • @HolyBlackCat Division of integers can't overflow, can it? So there's no need to cast anything. – Barmar Aug 14 '23 at 17:20
  • @Barmar division can overflow, but only for `INT_MIN / -1`. Integer division doesn't follow modular arithmetic anyway, so arguably, this case doesn't need to have wrapping behavior, but can remain silent UB or a signaling error. – Jan Schultke Aug 14 '23 at 17:21

1 Answers1

3

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.

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
  • It's not good enough to assert a twos-complement implementation since the language explicitly marks signed integer overflow as undefined behaviour. That means the compiler can assume a signed integer operation will never overflow and possibly remove functional code during optimization. This is most likely to hit you on loop conditions. UB is UB no matter what your expectations of what the compiler *should* do. – Stephen M. Webb Aug 14 '23 at 17:26
  • @StephenM.Webb my answer involves casting to unsigned integers, performing the operation, and then casting back. There is no undefined behavior anywhere in the process. The only detail of concern is that the conversion `unsigned -> signed` is implementation-defined prior to C++20. However, you can assert that two's complement is used by adding a `static_assert` which checks that this conversion has the intended effect. – Jan Schultke Aug 14 '23 at 17:31
  • @JanSchultke does this technique work for ALL integer arithmetic like division, modulo, bitwise operations (shifting et al), etc? – cppguy Aug 14 '23 at 19:16
  • @cppguy it works for bitwise operations and additive and multiplicative operators. It does not work for divison. For example, `-2 / -1 == 2`, but anything divided by `-1` (which is congruent to `UINT_MAX`) is zero. Basically, division is special; the rest works. – Jan Schultke Aug 14 '23 at 19:21
  • @JanSchultke does that mean that division/modulo don't need to cast to unsigned and back or is there another special solution for those operations? – cppguy Aug 14 '23 at 19:26
  • @cppguy "wrapping division" isn't really a thing anyway, so just use builtin division. The one case where division wraps (`INT_MIN / -1`) can't be meaningfully fixed with unsigned arithmetic either. However, you could guard against such division, so that it doesn't take place with an if-statement. – Jan Schultke Aug 14 '23 at 19:28
  • @JanSchultke doesn't `>>` work differently for signed vs unsigned. ie, it fills other bits with 0 or 1 depending on signedness? – cppguy Aug 14 '23 at 19:29
  • @cppguy yes, right-shifting is also different. However, bit-shifting is also not something that can wrap, so just use the builtin operation. See also https://stackoverflow.com/q/76495063/5740428 to make it "more portable" prior to C++20 – Jan Schultke Aug 14 '23 at 19:30
  • @JanSchultke what about modulo. Does that need the casting to/from unsigned? – cppguy Aug 14 '23 at 19:32
  • @cppguy yes, `%` gives you the remainder of the division, which also doesn't work when interchanging signed and unsigned types. – Jan Schultke Aug 14 '23 at 20:25
  • I've run across code written in the 80's where signed int wrapping worked and was depended on. It broke some 20 years ago. Rather than casting, I just added an unsigned zero to the initial argument forcing conversion for the rest to unsigned. Examples `i = 0u+i+i1;` where i and i1 are ints. Overflow wraps. This does require that signed ints are two's compliment but I've never worked on a system that it wasn't. – doug Aug 14 '23 at 21:25
  • @doug the `0u` is a neat trick (unless it surprises a collaborator), though it's worth saying that it wouldn't work if `i` and `i1` were `long` or `long long`. I'm not even sure if there are non-two's complement platforms with a C++ compiler. Probably not, so it's a good thing that the committee has standardized it. – Jan Schultke Aug 14 '23 at 21:35
  • @JanSchultke Yep. different sized ints are best handled by more explicit means. But the stuff I had to fix was just regular old int size types. I'm kind of torn. Anything more complex and I prefer explicit casts. But for the common simple cases where someone was actually depending on wrap, this seemed simpler and less clutter.. But it does assume the reader understands it forces conversion to unsigned. Some may not. Always a tradeoff for others (or yourself) reading the code down the road. – doug Aug 14 '23 at 21:45