Hacker News new | ask | show | jobs
by mehrdadn 2232 days ago
> the example which benefits from the way that Rust chooses to define it

I'm actually struggling to see what the practical benefit is in having it wrap around. The program is still producing garbage at that point, which you're not handling, so why not let the compiler just forget about that case just like you already did?

4 comments

The practical benefit is not in the wrapping specifically. The difference is in it being UB vs not UB; that is, overflow is a "program error" not UB, and so the language defines how implementations must handle this error.

You aren't supposed to rely on this semantic, as it's an error. If the checks get cheap enough, rustc will also check in release.

I mean, the post seemed to be arguing that the wrap-around is somehow beneficial, so I'm not sure you're saying the same thing. But if all you want is check + crash when that happens then can't you just use a compiler flag or a different data type or something else for that in C++?
It is beneficial that the behavior is defined, and therefore consistent. So you don't run into cases where it works on your laptop, but not on your server. Also, in this particular case, overflowing is probably not terrible, since you are computing a hash code. The worst case is that the distribution may not be as even as you hoped.
> It is beneficial that the behavior is defined, and therefore consistent. So you don't run into cases where it works on your laptop, but not on your server.

I already don't run into such cases though? Do you run into cases where changing an overflow to wrap-around (e.g. maybe via unsigned?) makes it suddenly your code work on 2 machines, whereas leaving it as overflow makes it work on 1 machine but not the other?

That was the example given in the original article
Yes, but I was asking about your experience.
I’m not saying that the wrapping behavior is beneficial. I’m saying that having it be not UB is beneficial.
But if all you want is check + crash when that happens then can't you just use a compiler flag or a different data type or something else for that in C++?
Different type? Sure. Disadvantage is that it’s not pervasive. Compiler flag? Probably, I am not an expert on compiler flags that change language semantics; we don’t do that in Rust, but I know C and C++ compilers do.
> Compiler flag? Probably, I am not an expert on compiler flags that change language semantics; we don’t do that in Rust, but I know C and C++ compilers do.

What do you mean? Is Rust's -C overflow-checks just a joke then? https://doc.rust-lang.org/reference/expressions/operator-exp...

Compiler flags are not part of the language, so not portable.

Using a different data type? You'd have to use arbitrary precision numeric data types to avoid this. After all, you can still overflow 64-bit ints.

I mean, if you're doing two's complement arithmetic as the example relies on then it's certainly useful. If you want any other behavior, then it is not. (Rust's differing behavior between different optimization levels IMO makes it basically impossible to use usefully, FWIW.)
How likely is that "if", though? How often when you see signed multiplication in code do you find people are actually paying attention to the negative results and planning around 2's complement behavior? I know there are occasions in which people do this, but it's so incredibly rare compared to when they multiply two numbers. And on the rare occasions when the bit patterns actually matter to them and they're paying attention, it's not like they have no way to solve the problem in C++.
If it wasn't clear, I'm saying that making the behavior defined is nice, but personally I think defining it to wrap is not the best way it could have been done. IMO a consistent trap would have been nicer.
> what the practical benefit is in having it wrap around

There's a pretty big practical difference between "getting an unexpected numerical result" and "letting an attacker steal my TLS keys and mine bitcoins on my machine."

How sure are you that that getting an unexpected numerical result isn't going to let an attacker take over your machine? In most code I see the behavior is just undefined and not paid attention to by the programmer, regardless of what the compiler is doing. A huge fraction of the time you'll just step out of the bounds of an array, just deterministically instead of nondeterministically. People don't pay attention to what happens in that case regardless except in like 0.01% of the time when they're writing some kind of bit manipulation magic, and in those rare cases they can handle them in C++ too, with a custom wrapper or an unsigned type or something.
> A huge fraction of the time you'll just step out of the bounds of an array, just deterministically instead of nondeterministically.

Well arrays are bounds checked by default in Rust, so you can't do that one. You're more likely to hit a crash, which I think is decidedly better than the compiler deciding to optimise out or rewrite your function because it contained UB.

> Well arrays are bounds checked by default in Rust, so you can't do that one.

But that's my entire point. If you're observing a benefit here, it's decidedly not due to the wrap-around, but due to other features (like bounds checking), and the argument should be that those features make Rust better than C++. It seems strange to give praise to the multiplication wrap-around instead.

I think I get what you're saying. The practical benefit of defining signed integer overflow in C, without any other changes, might not be very big for most C programs, since they're likely to get an out-of-bounds access either way. I'd add to that that undefined signed overflow makes it much harder to write good C code. Even if you carefully check all your array bounds, an unexpected integer promotion somewhere in there might lead the optimizer to delete the check you wrote.
C is a strawman though when we're discussing C++. Checking array bounds in C (or I'd argue, pretty much checking anything in C) is just a losing battle. The lack of proper abstraction facilities perpetually fights against you. C++ actually lets you abstract checks away into the definitions so that you don't have to modify every usage site.

Regarding optimizers deleting your checks though: is that something you encounter in practice, or just something you see people ranting about in blog posts? Can you even trigger this behavior if you try? Have you seen it happen more than once in a blue moon? I know on my end it's either never happened to me (likely) or it's been long enough ago that I have no memory of it. Even when I actively go out of my way to make this kind of thing happen, it gives me a hard time. Even the most blatant examples you'd try don't end up getting optimized out like this. Try [1] for example. It's both out of bounds and an uninitialized read, and yet the check is still there. If anything it's incredibly disappointing how bad optimizers are at optimizing out bounds checks!

[1] https://gcc.godbolt.org/z/i-FMV-

There is a real advantage to wraparound in that one can understand add, sub and mul as ring operations (as in common arithmetic), which makes it easier to understand the semantics of more complex expressions.