Recently, I was trying out
clippy â a linting and
static analysis tool for Rust, when I ran into a lint warning that wasnât
immediately clear to me:
warning: casting u8 to u16 may become silently lossy if types change.
For context, this is the snippet of code I ran through clippy:
fn main() {
let a: u8 = 0x12;
let b: u8 = 0x34;
assert_eq!(silly_max(a, b), b as u16);
}
fn silly_max(a: u8, b: u8) -> u16 {
let x = a as u16;
let y = b as u16;
if x > y {
x
} else {
y
}
}
Essentially, we take two 8-bit unsigned integers, cast them to 16-bit unsigned integers, and compare the casted results, returning the greater of the two.
Running clippy on this code, this is the lint error I received:
warning: casting u8 to u16 may become silently lossy if types change
--> src/main.rs:8:13
|
8 | let x = a as u16;
| ^^^^^^^^ help: try: `u16::from(a)`
|
= note: #[warn(cast_lossless)] on by default
= help: for further information visit https://rust-lang-nursery.github.io/rust-clippy/v0.0.212/index.html#cast_lossless
Now, the issue with this code isnât immediately obvious. And thatâs because this code is âcorrectâ in that, at present, it does what it should do.
The potential error, as clippy suggests, is what could happen if I later
change this code. Suppose in the future, the design of the program changes and I
actually want to find the max of two u32s. Look what would happen:
fn main() {
let a: u32 = 0x00000002;
let b: u32 = 0x10000001;
assert_eq!(silly_max(a, b), b as u16);
}
fn silly_max(a: u32, b: u32) -> u16 {
let x = a as u16;
let y = b as u16;
if x > y {
x
} else {
y
}
}
When we try to run this code, we get:
Running `target/debug/silly_max`
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `2`,
right: `1`', src/main.rs:4:5
Clearly something went wrong. This is a classic truncation issue. When we cast
u32->u16, we lose the top 16 most significant bits of our integers.
0x10000001 gets truncated to 0x0001 and 0x00000002 becomes 0x0002. Under
truncation, our comparison operation doesn’t always work how we’d expect.
Luckily in this case, the truncation was rather clear. However, our code still
compiled. We only noticed at runtime that weâd silently introduced a bug.
(Just as clippy warned)
Letâs go back to the first version and implement the fix clippy suggested,
using u16::from instead of casting via as.
fn silly_max(a: u8, b: u8) -> u16 {
let x = u16::from(a);
let y = u16::from(b);
if x > y {
x
} else {
y
}
}
The above code compiles and works as expected. Now, lets make the same code
change (switching the types of a and b to u32).
fn silly_max(a: u32, b: u32) -> u16 {
let x = u16::from(a);
let y = u16::from(b);
if x > y {
x
} else {
y
}
}
Now, we get a friendly compiler error:
--> src/main.rs:8:13
|
8 | let x = u16::from(a);
| ^^^^^^^^^ the trait `std::convert::From<u32>` is not implemented for `u16`
|
= help: the following implementations were found:
<u16 as std::convert::From<bool>>
<u16 as std::convert::From<u8>>
This is what we want! Instead of silently introducing the possibility of truncation into our program, the compiler lets us know that weâre trying to do an unsafe translation.
But what if we want this possible truncation behavior? Well, you can still use
as and you’ll get truncated casts. Interestingly, clippy doesnât complain if
you do u32 -> u16 via as â it assumes you know what youâre doing.
So, whatâs the moral of the story?
Prefer using the
std::convert::Fromtrait for doing type conversions. Theaskeyword performs âsafeâ casts (casting wonât cause a panic), but you can still run into classes of errors that can be avoided by using designated converter methods.
Here are some resources if youâre interested in learning more: