Clippy's "cast_lossless" Lint Warning

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::From trait for doing type conversions. The as 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: