Signed integer addition is only associative when overflow is defined to wrap around like unsigned arithmetic. This condition is matched here, because only debug builds panic on overflow. However, it's a bit of a gray area that the article completely ignores.
> Rust's signed arithmetic is fully specified to wrap around.
Well, kind of. It's currently documented to wrap in release mode by default, but it's just that - a default. You're free to enable overflow checks in release mode (or disable them in debug if you really like oddball configurations), and either way overflow is considered a logic error that devs shouldn't rely on (and basically can't rely on when not in control of the end binary since it's the end user who controls overflow checks).
The Rust devs are theoretically open to making signed overflow panic by default, but consider such a change unlikely unless "something materially changes" [0].
Thanks for clarification, it seems Rust devs (as opposite to C devs) like good defaults and don't like making code accidentally cut yourself just because you looked at it wrong
This deserves elaborating on, because it's pretty cool.
Rust has two behaviours around overflow. In debug builds, it panics, in release builds it wraps.
IMO wrapping is a reasonable-enough behaviour to avoid UB in release builds, and panicking in debug is definitely the correct behaviour, because you're only avoiding UB by defaulting to something, but that's not nearly enough. In most applications where overflow is a risk you should make sure to choose what behaviour you consider correct.
Thankfully Rust has a pretty robust story around this:
fn add_behaviour() {
let small: i32 = 123;
let big: i32 = i32::MAX;
assert_eq!(small.wrapping_add(big), i32::MIN + 122);
assert_eq!(small.overflowing_add(big), (i32::MIN + 122, true));
assert_eq!(small.overflowing_add(small), (246, false));
assert_eq!(small.saturating_add(big), i32::MAX);
assert_panics!(a.strict_add(b)); // (nb: Not a real assertion)
}
And you could easily implement the default behaviour yourself with conditional compilation:
No. If you want wrapping, ask for it with Wrapping<T> or the specific Wrapping types, or the wrapping arithmetic APIs
It's true that since it's safe and faster, release builds default to wrapping rather than panic, but it's still wrong if you overflow any of Rust's default integer types, it's just that in a safe language it won't be Undefined Behaviour.
"I can't be bothered to do it correctly" speaks to the quality of the rest of the product, it's a Brown M&M [read about the Van Halen test if you don't know what a Brown M&M means]
Probably never. This has been debated to death by both C and C++ standards committees. The consensus is that, since signed overflow is almost always a sign of a bug in the program, keeping it undefined enables compilers to optimize by assuming it never happens, and also allows sanitizers to continue to flag it to developers so that they fix their bugs (though it could be argued that, a sanitizer doesn't really have to strictly adhere to the standard, the committee apparently didn't feel that way).
> and also allows sanitizers to continue to flag it to developers so that they fix their bugs
I've never really found this argument particularly convincing; as you say, sanitizers don't have to strictly adhere to the standard, and they do in fact take advantage of this flexibility to check behaviors that "are not undefined behavior, but are often unintentional" (e.g., -fsanitize=unsigned-integer-overflow).
Makes me wonder whether "sanitizers can't flag defined behavior" is meant to be shorthand for some more nuanced position ("the false positive rate for signed overflow sanitizer checks would be too high", maybe?) or something else.
Of course, -fsanitize=unsigned-integer-overflow isn't enabled by default, and few people use it (github code search gives 6K results for that, compared to 175K for "-fsanitize=undefined"; which to be fair is a lot higher than I expected, but still not a lot).
> Makes me wonder whether "sanitizers can't flag defined behavior" is meant to be shorthand for some more nuanced position
And signed overflow checking would have to be off-by-default too, if people were allowed to start relying on it. It'd be less "false positive rate too high", more "it disallows you to use a genuine language feature that is actually useful", defeating the point of defining signed overflow in the first place.
(imo defining signed overflow specifically for reducing attack surface from exploitable UB is a mostly-separate discussion, which should not affect core language semantics, and certainly not what users would be suggested to do)
> Of course, -fsanitize=unsigned-integer-overflow isn't enabled by default, and few people use it
Sure, but it's still a counterexample for "you can't define it because it means sanitizers can't warn for it". Sanitizers can warn for it; you "just" get a worse signal-to-noise ratio.
> It'd be less "false positive rate too high", more "it disallows you to use a genuine language feature that is actually useful"
I'm not sure I see the distinction? Flagging a correct use of a language feature as incorrect is more or less the definition of a false positive, so if intentional signed overflows get a reasonable amount of use then that'd presumably result in an unacceptably noisy check to be enabled by default.
> defeating the point of defining signed overflow in the first place.
As for unsigned overflow checks I'd imagine the intent is that one would enable that particular check if you think that the corresponding overflow is more likely to be unintentional than not, and in the cases where it actually is intentional you can suppress the check.
> it's still a counterexample for "you can't define it because it means sanitizers can't warn for it"
Sure, technically you can write a sanitizer for anything. It just becomes less a "sanitizer" you can always recommend everyone everywhere use, and more of just a heuristic thing that only really works if you design your code for its arbitrary desires.
> and in the cases where it actually is intentional you can suppress the check.
imo it'd be nice to have separate types for wrapping and non-wrapping integers for that, so that you have actual language-level semantics and an easy way to mix things (e.g. wrapping arith for hashing, mixed with non-wrapping arith for loop index or whatever) instead of suppressions.
> It just becomes less a "sanitizer" you can always recommend everyone everywhere use, and more of just a heuristic thing that only really works if you design your code for its arbitrary desires.
Sure, and that's basically what I was wondering about with respect to "can't define it" being shorthand for something else
> imo it'd be nice to have separate types for wrapping and non-wrapping integers for that
I think I'd agree, though I'd imagine it's a bit late for such things to be deeply integrated into the language. At least making your own isn't horrendously difficult.
> Sure, and that's basically what I was wondering about with respect to "can't define it" being shorthand for something else
Eh, I'd say it's still the same thing; can't define a sanitizer for it if what you define isn't a sanitizer. Depends on a specific definition of "sanitizer" though.
> At least making your own isn't horrendously difficult.
In C++ perhaps, but impossible in C. (and there are still some funky edge-cases where multiplying two `uint16_t`s can overflow due to implicit promotion to signed int; C's _BitInt solves at least that)
> Depends on a specific definition of "sanitizer" though.
Hrm, I suppose a general definition would be something that you use to check for certain (unintended?) runtime behaviors? Though I also feel that could include hardened implementations and stuff like valgrind....
Making signed overflow EB would preclude using signed overflow for optimization though, which seems to be (correctly or not) considered an important use case.