5 ms·
I did fairly well in the test (I didn't remember that you couldn't shift into the sign bit), but it really helps highlight how stupid the C promotion rules are.
by simias 4y ago
I did fairly well in the test (I didn't remember that you couldn't shift into the sign bit), but it really helps highlight how stupid the C promotion rules are. In C as soon as I have to do arithmetics with anything but unsigned ints all sorts of alarm bells start going off and I end up casting and recasting everything to make sure it does what I think it does, coupled with heavy testing. Now add floats and doubles into the mix and all bets are off.
Rust not having a default "int" type and forcing you to explicitly cast everything is such an improvement. Truly a poster child for "less is more". Yeah it makes the code more verbose, but at least I don't have to worry about "lmao you have a signed int overflow in there you absolute nincompoop, this is going to break with -O3 when GCC 16 releases 8 years from now, but only on ARM32 when compiled in Thumb mode!"
- tialaramex 4y agoStill, I would rather not have Rust's 'as' cast. I should like to see all, or at least most uses of 'as' deprecated in a later Rust edition. Rust's 'as' will silently throw away data to achieve what you asked. Sometimes you wanted that, sometimes you didn't realise, and requiring into() and try_into() instead helps fix that. For example suppose I have a variable named beans, I forgot what type it is, but it's got the count of beans in it, there should definitely be a non-negative value because we were counting beans, and it shouldn't be more than a few thousand, so we're going to put that in a u16 variable named enough. let enough = beans as u16; This works just fine, even though beans is i64 (ie a signed 64-bit integer). Until one day beans is -1 due to an arithmetic error elsewhere, and now enough is 65535, which is pretty surprising. It's not undefined but it is surprising. If we instead write let enough = beans.into(); we get a compiler error, the compiler can't see any way to turn i64 into u16 without risk of loss, so this won't work. We can write beans.try_into().unwrap() and get a panic if actually beans was out of range, or we can actually write the try_into() handler properly if we realise, seeing it can fail, what we're actually dealing with here.
- bestouff 4y agoI feel you. I'd really like a way for "as" to be transformed into "try_into().unwrap()", say in a later edition. Or even just "into()" which would throw a nice compile-time error - This is the Rust Way.
- jcranmer 4y agoIf I can get away with using .into() over as, I will. However, there are so many cases where you can't, and .try_into().unwrap() is just way too unwieldy. In particular, there's no way for me to go "I know I'm only ever going to run on 64-bit machines where sizeof(usize) == sizeof(u64), so usize should implement From<u64>" (and even more annoying, I might be carefully making sure this works on 32-bit and 64-bit machines and get screwed because Rust thinks it could be portable to a 16-bit usize despite the fact I'm allocating hundreds of MB of memory). And of course there are times I do want to bitcast negative values to large positive unsigned values or vice versa, without error. So while I do understand why maybe you shouldn't use as, at the end of the day, it just ends up being easier to use it than not use it.
- gspr 4y ago> try_into().unwrap() is just way too unwieldy. I always carry with me a tiu() alias method for exactly that reason (in a TryIntoUnwrap trait implemented for every pair of types which implements TryInto).
- arcticbull 4y agoYou can also do try_from()? if you impl From<TryIntError> on your local error type.
- ridiculous_fish 4y agoA flip side is that some safe conversions also produce compiler errors. On a 64-bit system, usize and u64 are the same width, but they are not convertible: neither is Into the other, so you cannot lean on the compiler in the way you describe. You might use try_into(), but then you risk panicking on a 32-bit system, instead of getting a compile-time error.
- WalterBright 4y agoThe trouble with explicit casting is if the code is refactored to change the underlying integer type, the explicit casts may silently truncate the integer, introducing bugs. D follows the C integral promotion rules, with a couple crucial modifications: 1. No implicit conversions are done that throw away information - those will require an explicit cast. For example: int i = 1999; char c = i; // not allowed char d = cast(char)i; // explicit cast 2. The compiler keeps track of the range of values an expression could have, and allows narrowing conversions when they can be proven to not lose information. For example: int i = 1999; char c = i & 0xFF; // allowed The idea is to safely avoid needing casts, in order to avoid the bugs that silently creep in with refactoring. Continuing with the notion that casts should be avoided where practical is the cast expression has its own keyword. This makes casting greppable, so the code review can find them. C casts require a C parser with lookahead to find. One other difference: D's integer types have fixed sizes. A char is 8 bits, a short is 16, an int is 32, and a long is 64. This is based on my experience that a vast amount of C programming time is spent trying to account for the implementation-defined sizes of the integer types. As a result, D code out of the box tends to be far more portable than C. D also defines integer math as 2's complement arithmetic. All that 1's complement stuff belongs in the dustbin of history.
- simias 4y agoYeah for sure, having more expressive casts and differentiating between "upcasts" and "downcasts" is definitely better. My point that even "lossy" casts are better than C's weird arcane implicit promotion rules by quite a margin IMO. Rust's current handling of this issue is by no mean perfect, although it's been steadily improving and I definitely don't use `as` as much as I used to. >D also defines integer math as 2's complement arithmetic. I think modern C standards does so as well, but I think than signed overflow is still UB, so it's mostly about defining signed-to-unsigned conversions. There are flags on many compilers to tell them to assume wrapping arithmetic but obviously that's not standard...
- Measter 4y ago> The trouble with explicit casting is if the code is refactored to change the underlying integer type, the explicit casts may silently truncate the integer, introducing bugs. That depends on how the casting is provided. For the C-style casting, or Rust's `as` casting, yes that is a problem. However, another way casting could be provided is through conversion functions that are only infallible if information isn't lost. For example, let's say we have the functions `to_u16` and `to_i16`. For an `i8` the first function could return `Option<u16>`, the second `i16`, while for a `u8` they would return `u16` and `i16`. That way, any change to the types that could cause it to now silently truncate would instead cause a compiler error because of the type mismatch. Rust almost gets there with its `Into` and `TryInto` traits which do provide that functionality, but trying to use them in an expression causes type inference to fail, which just makes them a pain in the ass to use.