Skip to content

← Ground Rules: Values, Types, Control Flow step 10 of 24

Medium Primitives

Casting with as: truncation, sign reinterpretation, saturation

Narrow each value to a u8 and widen it straight back to i64. Report what survived the round trip.

pub fn cast_report(values: &[i64]) -> Vec<i64>

300 comes back as 44. -1 comes back as 255. 0 stays 0. 256 comes back as 0.

The starter fails clippy on a cast that does nothing, which is a small joke at the expense of the operator this whole item is about.

as never fails, and that is the problem

as is the most dangerous operator a Rust beginner reaches for, because it always compiles, never returns an error, never panics, and never warns by default. It is the one place in the language where a value can silently become a different value.

Integer to narrower integer: keep the low bits.

300i64 as u8    // 44   — 300 - 256
256i64 as u8    // 0
-1i64  as u8    // 255  — two's complement, all ones

Integer to wider integer: sign-extend if the source was signed.

-1i8 as i32     // -1     (sign-extended)
-1i8 as u32     // 4294967295  (reinterpreted)

Same width, different sign: reinterpret the bits, do not change them.

255u8 as i8     // -127 - 1 = -128? no: -1. The byte 0xFF read as signed.

None of this is undefined. All of it is a place where your number quietly became a different number.

Float to integer saturates — since 1.45

This is the corner people get wrong from old memory, so be precise about it. As of Rust 1.45, f64 as i32 saturates, and it is fully defined:

1e10_f64  as i32   // 2147483647   = i32::MAX  (saturates, does not wrap)
-3.99_f64 as i32   // -3           (truncates toward zero, does not round)
f64::NAN  as i32   // 0
-1e10_f64 as i32   // -2147483648  = i32::MIN

Before 1.45 the out-of-range cases were undefined behaviour, and a great deal of internet folklore still says so. It is not true on any compiler you will meet today. Note also that the fractional case truncates toward zero — -3.99 becomes -3, not -4.

What clippy does and does not tell you

Here is the uncomfortable part. The two lints that would warn you about the dangerous cases —

  • cast_possible_truncation — “casting i64 to u8 may truncate”;
  • cast_sign_loss — “casting i64 to u8 may lose the sign”;

— are both in the pedantic group, which is off by default. So out of the box, clippy teaches you nothing whatsoever about the single most dangerous operator in the language. That is worth knowing about the tool: it is opinionated, its defaults are tuned for low noise, and low noise is not the same as high safety.

What is on by default is unnecessary_cast, which fires when a cast has no effect at all — v as i64 where v is already i64. That is the starter’s bug, and it is a real one: a redundant cast tells the reader a conversion is happening when none is.

Writing it

Narrow with as u8, then widen back. For the widening step, prefer i64::from(byte) over byte as i64 — the From impl exists precisely because that direction is always exact, and using it documents that no information can be lost. Item 1.11 turns this preference into a rule.