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 u32
s. 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::From
trait for doing type conversions. Theas
keyword 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: