When it comes to unsigneds, there's no such problem (and this is in fact the real reason why anything that can be unsigned in C/C++ should be unsigned).
There's also explicit arithmetic functions that let you choose your overflow behavior:
* Checked: return None on overflow
* Wrapping: two's compliment wrapped overflow (the "overflowing" variant gives you a carry bit)
* Saturating: return maximum value on overflow
int would_increment_overflow(int a) {
if (a + 1 < a) {
return -1;
return 0;
}
This gets turned to a function that always returns 0. 0000000000000000 <would_increment_overflow>:
0: f3 0f 1e fa endbr64
4: 31 c0 xor %eax,%eax
6: c3 retq
Using unsigned int gives the wrapping semantics.It can not, because "compiler optimization passes" is not the root cause of this behavior: it is that it is undefined behavior in the language itself. This is what gives the compiler license to make those transformations. In Rust, it is not undefined behavior, and therefore the compiler does not have the right to make those transformations for Rust code.
Just because Rust uses LLVM does not mean that suddenly it inherits C's semantics. This has actually happened (well, more specifically, C++'s semantics, C was accidentally inheriting them as well IIRC) at least one time before, but that is a bug in LLVM that was fixed. Now each language has the proper semantics here, and it all works just fine. (I am referring to the behavior with regards to infinite loops with no side effects.)
int8_t abs8(int8_t n) {
return (n >= 0) ? n : -n;
}
Even with `-ftrapv`, evaluating abs8(-128) will produce -128, because 127 is the maximum value for a signed 8-bit integer (so trying to get 128 wraps around to -128), but this isn't caught. However, both Rust and Ada do catch this. This article highlights this difference between C and Ada:https://borretti.me/article/signed-integers-asymmetrical
I know Rust catches it because after I read that article, I tested it with Rust in debug mode.
It is easy enough to crank out custom integer types in C++ these days which are arbitrarily safe that there isn’t much excuse for not doing it, particularly since generics and metaprogramming does most of the work. Outside of interfacing with syscalls, there isn’t much use for C primitives beyond size_t.
I didn't know this. I can imagine you can create your own integer types as wrappers over existing primitives, and define them so that, e.g., on signed overflow, the program aborts or wraps (and your signed integers are actually a wrapper class over unsigned integers, but with operator overloading so that they act like signed integers). Is that what you mean? If not, could you show me what you mean?
This did not work well in older versions of C++ because the limited type inference meant that many common cases around different type interactions required explicit handling that made it clear you were not using native types. Around C++17 it became possible to define integer types that were almost entirely indistinguishable from the native types in ordinary C++ code.
C++17 was important because it meant a lot of safety in the code base could become automagic via the type system, especially for primitive types. The code looks the same whether you are using it or not. That was a huge capability change. C++20 then generalized it to arbitrarily complex types. It is difficult to overstate how much this improves the conciseness and safety of non-trivial code.
There are likely multiple necessary changes for it to be fast. One part is implementing the check quickly, this could be hardware assisted. Another is addressing all the optimizations the compiler does by asuming that there is no overflow, and figuring out alternative ways to get the compiler to emit fast code.
Trapping is nonsense - in production anyway. You might use it in development to catch errors. But in production there is no way to "fix" an overflow if it happens and is detected, so you'd be looking to crash on trap which in some code is preferable to silent data corruption.
I do lots of fixed-point math for embedded motor control. Representing rotor angle as a signed 16bit number is deeply engrained in me these days because taking differences to get a signed delta just works, and I never have to worry about rollover because it behaves exactly the way I want. ;-) While this is technically undefined behavior, I've never run across a compiler that worked differently.
Undefined behaviour can cause miscompiles which can break the expected logic of a program. In the worse case, maybe the compiler optimises away your password validation because it has decided that the failure branch is undefined behaviour.
Non determinism can lead to logic bugs, but it can't magically introduce new logic into the program like UB can as part of optimising