Unsigned and signed ints: From trait vs casting

I am a little confused about casting between signed and unsigned integers in Rust.
Here is a scenario I have in mind: I have an unsigned constant CONSTANT (less than 32 bits required to represent) and I have to pass it to some functions that expect an i64 and some that expect a u64.
There seem to be two ways of implementing this in Rust:
1.

const CONSTANT: u32 = 160;
function_that_requires_u64(u64::from(CONSTANT));
function_that_requires_u64(i64::from(CONSTANT));
const CONSTANT: u64 = 160;
function_that_requires_u64(CONSTANT);
function_that_requires_i64(CONSTANT as i64);

Intuitively, I tend to prefer the first option, because it seems more explicit. My intuition regarding the second version is that I might get a problem when the constant is actually something larger than what fits into an i64 (maybe u64::MAX).
Can someone explain what the (internal) difference between the two versions is? Which one do you prefer, and why? Can you think of (edge) cases where they have different behaviour?

The From::from conversion accepts only what are considered to be infallible conversions while as accepts conversions that, well, don't quite fail but truncate or wrap the value. So if you want to guarantee that the value that is being converted is not truncated or wrapped around, use From. The clippy lint clippy::checked_conversions can help enforce the use of From and otherwise TryFrom methods over as.

The use of From over as is more "robust to change" elsewhere in the code. You would not be able to change the definition of CONSTANT to a type that can not be converted to an u64 and i64 without risking truncation or wrapping. For this reason alone, and without further context, I would generally recommend to use From.

2 Likes

This is the part that I don't understand 100%. Where can I find more information on what exactly casting with as does for ints?

It just changes the type but results in the identical bit pattern. This is in the Reference.

Hmm, this seems quite formal, and I can't seem to find the answers to what coercions mean for integers specifically.

Here:

Have you even tried to read the link? Semantic part looks crystal clear to me.

1 Like

I'd also prefer the first option and so does pedantic clippy. This lint catches casts that could possibly wrap the value when casting from an unsigned to a signed integer type with the same size. Wrapping would happen in your second case if your constant is bigger than i64::MAX, which would result in funny integers like u64::MAX as i64 == -1.

1 Like

Ah sorry, my bad. I read everything in the type casts expression section until then, as I thought it ended there.

You can write this in a slightly shorter way:

const CONSTANT: u32 = 160;
function_that_requires_u64(CONSTANT.into());
function_that_requires_i64(CONSTANT.into());

If you go with option 2, I wouldn't use the as operator. Instead, use try_into:

const CONSTANT: u64 = 160;
function_that_requires_u64(CONSTANT);
function_that_requires_i64(CONSTANT.try_into().unwrap());

The as operator will silently convert values modulo 264 (so u64::MAX becomes -1) which is very rarely what you want, and for that reason people have even been talking about deprecating this operator in this context.

try_into().unwrap() will panic at runtime if the number is too large, which is not optimal (you'd prefer to fail at compile time), but better than silently getting a different number.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.