C can't even do all of integral arithmetic safely. It's a language that goes really out of its way to add unsafety.
C can't even do all of integral arithmetic safely. It's a language that goes really out of its way to add unsafety.
But integer arithmetic is safe in terms of Rust.
> Checking every addition adds a massive slowdown
It only does so for debug mode. In release mode, it uses modular arithmetic.
> In mathematics, modular arithmetic is a system of arithmetic for integers, where numbers "wrap around" when reaching a certain value, called the modulus. The modern approach to modular arithmetic was developed by Carl Friedrich Gauss in his book Disquisitiones Arithmeticae, published in 1801.
Only some of the integral types in C are modular. If they all where, it wouldn't be a problem.
I was tempted to specify "unsigned" to ward off the obnoxious pedants, and I see that I should have. C really should be a portable assembly language by now. A very small non-breaking change to the standard, and C's arithmetic would be the same as Rust's.
Yeah, that's also astonishing.
Anyway, I've stopped blaming the C developers by now. I just assume they have the goal of killing the language and moving people into a more ergonomic alternative. I don't know their true intentions, but this has been a very predictive assumption.
(I guess any definition of any UB would be non-breaking, so yeah, they could fix all of the language.)
Sadly, that's my conclusion too. I really wish there was a good "portable assembly language", and maybe that'll be something that targets WASM.
There isn't a guarantee that any given standards-conforming compiler will, but it seems that with Rust there isn't a guarantee what behaviour you get either (it depends on the compile settings). In either language, you can't write code that does signed overflow in a meaningful way (at least not if you use Debug).
https://doc.rust-lang.org/std/primitive.i64.html#method.wrap...
I'd rather just put `-fwrapv` on the command line that clutter my code with crap like that though.
The advantage of Rust's way is that it lets you customise addition on a per-operation basis. So you can mix and match wrapping addition with saturating addition, etc.
int64_t x = add_i64_with_abort_on_debug_and_wrap_on_release(y, z);
uint8_t t = add_u8_with_saturation(u, v);
I'd prefer the default for the math operators be what all the CPUs currently do, and neither C or Rust promises that.So either A) the optimizations using UB are important, and therefore C is/will-always-be faster than Rust which doesn't have them. Or B) the optimizations using UB are not important and the compiler writers for gcc and clang are wrong.
You pick.
And of course some insane people advocate for adding undefined behavior to Rust in the name of optimizations. Gross.
To expand on this: integer overflow is not UB, it is unspecified. It can result in clamping, wrapping or a panic, depending on configuration at compile time.
> In release mode, it uses modular arithmetic.
And I believe that to have been a mistake. Android enables overflow checks by default and there is no measurable performance impact.
So it's still treated as an error, just one that has a predictable fallback. I'm really not sure how that's much different from `-fsanitize=undefined`. Broken code is broken, even if it breaks in a predictable manner.
Now if the modular arithmetic had been enshrined as the expected behavior without being treated like an error to be caught, it'd be another matter.
Signed overflow is not an error in C.
A chunk of C that causes a signed overflow has an error in it. Seemingly, so does Rust code according to the behavior described in the post I was replying to.
My point is that I question how big is the value gain from having a predictable fallback when we are already within the realm of "this code is considered wrong". This isn't unlike the various arguments against the value of compiler warnings.
That being said, I agree that it's preferable in general, but the difference seems rather marginal to me. That is, within the context of what I'm replying to. I wouldn't be surprised if Rust had a few additional tricks up its sleeve to address this.
Because a caught static error, a runtime error, a wrong value, and C's UB are completely different beasts.
Anyway, none of those are anything nearly as damaging as C's UB. All of them are reasonable, on the literal sense that you can reason about them, anticipate what your program may do, and defend against the problem (or shrug it off and claim "it doesn't matter here"). You can do neither with by the spec C.
Example: https://godbolt.org/z/Kvrrx19Pa
The UB in the spec is exactly what makes safe use possible without enforcing it everywhere, which is not feasible for C.
It's "defined safety": If a >= 0 and b >= 0 then a + b > = 0. True according to most schoolchildren but not true according to the Rust spec. It breaks the principle of least astonishment and has and will lead to security vulnerabilities.
Your comment reads like nonsense. Are you able to provide what you feel is the best example that substantiates your claim?
Since this is a discussion about what C is and is not, I think it's fair to limit ourselves to the actual language as-is.
> I think it's fair to limit ourselves to the actual language as-is
I don't agree. Standard C only ever seems to be discussed when some asinine undefined behavior starts causing problems that we have to work around. No one cares about it otherwise.
It's better to redefine C as whatever the compilers accept. Now we can actually move forward and actually fix problems such as "signed integer overflow is undefined" -- just tell the compilers to start dealing with it.
At its heart, behaviour that's undefined like this is a source of optimisation opportunities, because we tend (and especially the preprocessor tends) to write code that assumes it won't happen. C does not lend itself well to iterator patterns that elide the range check entirely, and so it is valuable to the optimiser to be able to assume that a variable that steadily increments will not suddenly turn out to have wrapped. So we may (for example) unroll memory accesses[0], secure in our understanding that when we add 1 three times we will get three consecutive numbers, and (where we would normally add another 1) go ahead and add four at the end of the unrolled loop.
If we trap at that point, the behaviour is different from if we accessed each memory location in turn and trapped when we actually wrapped around. In the C model, the behaviour is still clasically undefined, but we've added a trap to hopefully catch that it happened before running too much further. We still can't assert anything about the state of the program after the trap, to potentially recover from it.
People writing performance-sensitve code get frustrated when a compiler trades performance for safety, so we're probably never going to get "safe" C in that sense. In practice though? This form of undefined behaviour only kicks in at runtime. Make sure your software is bug-free and you need never worry about it.
[0]: Imagine you have a 16 bit signed int that you're using as input into computing an index into an array -- you may know that it's never going to overflow, but how do you tell the compiler?
So if you have a = -1, b = 1000 and compare the two, a > b is actually true.