Hacker News new | ask | show | jobs
by saagarjha 2237 days ago
> As you can see, the documented behavior and the absence of undefined behavior due to signed overflows do make life easier.

Not having undefined behavior does make life easier, but having it be defined and then giving the example which benefits from the way that Rust chooses to define it is not really fair.

> With less effort, Rust generates less assembly code. And you don't need to give any clues to the compiler by using noexcept, rvalue references and std::move. When you compare languages, you should use adequate benchmarks.

Actually, the issue is that C++ just can't match Rust's semantics here. By default it will allow for exceptions, by default it will copy; if you watch the talk rvalue references cause the double indirection and fixing this would require some changes in the language to accommodate the use case.

2 comments

> 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?

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
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 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.
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.
> Not having undefined behavior does make life easier

I have mixed feelings about this. It seems like many examples of undefined behavior are things that don't make sense to do (ie ought to result in an error) which would often be undecidable at compile time. Runtime error checks would incur performance penalties so it's up to to programmer to include them.

The borrow checker is an obvious advantage here. Beyond that, I guess you could do many of the same things in unsafe blocks and they could cause problems just as easily. If they end up breaking things no one will blame the language because the tin was clearly labeled.

I do prefer language designs that make it possible to write things that are safe by default. It just seems like many problems are misattributed to undefined behavior but are actually due to systemic issues in the design of the language.

> Runtime error checks would incur performance penalties

Which is why in rust, there are many runtime checks done in debug builds but not in release builds. (Including checking for wraparound and bounds checks).

The idea being that automated tests should catch the errors, assuming you wrote good tests.

Bounds checks are absolutely done in release builds. The optimizer is more likely to be able to remove ones that are irrelevant, but the checks are not turned off.