Integers

85. cast_signed & cast_unsigned — Explicit Sign Casting for Integers

Stop using as to flip between signed and unsigned integers — cast_signed() and cast_unsigned() say exactly what you mean.

The problem with as

When you write value as u32 or value as i64, the as keyword does too many things at once: it can change the sign, widen, truncate, or even convert floats. Readers have to mentally verify which conversion is actually happening.

1
2
let x: i32 = -1;
let y = x as u32;  // Sign cast? Truncation? Widening? All of the above?

The fix: cast_signed() and cast_unsigned()

Stabilized in Rust 1.87, these methods only reinterpret the sign of an integer — same bit width, same bits, just a different type. If you accidentally try to change the size, it won’t compile.

1
2
3
4
5
6
let signed: i32 = -1;
let unsigned: u32 = signed.cast_unsigned();
assert_eq!(unsigned, u32::MAX); // Same bits, different interpretation

let back: i32 = unsigned.cast_signed();
assert_eq!(back, -1); // Round-trips perfectly

The key constraint: these methods only exist between same-sized pairs (i32u32, i64u64, etc.). There’s no i32::cast_unsigned() returning a u64 — that would silently widen, which is exactly the kind of ambiguity these methods eliminate.

Where this shines

Bit manipulation is the classic use case. When you need to treat an unsigned value as signed for arithmetic and then go back, the intent is crystal clear:

1
2
3
4
5
6
fn wrapping_distance(a: u32, b: u32) -> i32 {
    a.wrapping_sub(b).cast_signed()
}

assert_eq!(wrapping_distance(10, 3), 7);
assert_eq!(wrapping_distance(3, 10), -7);

Compare that to the as version — a.wrapping_sub(b) as i32 — and you can see why reviewers love the explicit method. It’s one less thing to second-guess in a code review.

#082 Apr 2026

82. isqrt — Integer Square Root Without Floating Point

(n as f64).sqrt() as u64 is the classic hack — and it silently gives the wrong answer for large values. Rust 1.84 stabilized isqrt on every integer type: exact, float-free, no precision traps.

The floating-point trap

Converting to f64, calling .sqrt(), and casting back is the go-to pattern. It looks fine. It isn’t.

1
2
3
4
let n: u64 = 10_000_000_000_000_000_000;
let bad = (n as f64).sqrt() as u64;
// bad == 3_162_277_660, but floor(sqrt(n)) is 3_162_277_660 — or is it?
// For many large u64 values, the f64 round-trip is off by 1.

f64 only has 53 bits of mantissa, so for u64 values above 2^53 the conversion loses precision before you even take the square root.

The fix: isqrt

1
2
3
4
let n: u64 = 10_000_000_000_000_000_000;
let root = n.isqrt();
assert_eq!(root * root <= n, true);
assert_eq!((root + 1).checked_mul(root + 1).map_or(true, |sq| sq > n), true);

It’s defined on every integer type — u8, u16, u32, u64, u128, usize, and their signed counterparts — and always returns the exact floor of the square root. No casts, no rounding, no surprises.

Signed integers too

1
2
3
4
5
6
let x: i32 = 42;
assert_eq!(x.isqrt(), 6); // 6*6 = 36, 7*7 = 49

// Negative values would panic, so check first:
let maybe_neg: i32 = -4;
assert_eq!(maybe_neg.checked_isqrt(), None);

Use checked_isqrt on signed types when the input might be negative — it returns Option<T> instead of panicking.

When you’d reach for it

Perfect-square checks, tight loops over divisors, hash table sizing, geometry on integer grids — anywhere you were reaching for f64::sqrt purely to round down, reach for isqrt instead. It’s faster, exact, and one character shorter.

81. checked_sub_signed — Subtract a Signed Delta From an Unsigned Without Casts

checked_add_signed has been around for years. Its missing sibling finally landed: as of Rust 1.91, u64::checked_sub_signed (and the whole {checked, overflowing, saturating, wrapping}_sub_signed family) lets you subtract an i64 from a u64 without casting, unsafe, or hand-rolled overflow checks.

The problem

You’ve got an unsigned counter — a file offset, a buffer index, a frame number — and you want to apply a signed delta. The delta is negative, so subtracting it should increase the counter. But Rust won’t let you subtract an i64 from a u64:

1
2
3
4
5
let pos: u64 = 100;
let delta: i64 = -5;

// error[E0277]: cannot subtract `i64` from `u64`
// let new_pos = pos - delta;

The usual workarounds are all awkward. Cast to i64 and hope nothing overflows. Branch on the sign of the delta and call either checked_sub or checked_add depending. Convert via as and pray.

The fix

checked_sub_signed takes an i64 directly and returns Option<u64>:

1
2
3
4
5
let pos: u64 = 100;

assert_eq!(pos.checked_sub_signed(30),  Some(70));   // normal subtraction
assert_eq!(pos.checked_sub_signed(-5),  Some(105));  // subtracting negative adds
assert_eq!(pos.checked_sub_signed(200), None);       // underflow → None

Subtracting a negative number “wraps around” to addition, exactly as the math says it should. Underflow (going below zero) returns None instead of panicking or silently wrapping.

The whole family

Pick your overflow semantics, same as every other integer op:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
let pos: u64 = 10;

// Checked: returns Option.
assert_eq!(pos.checked_sub_signed(-5),  Some(15));
assert_eq!(pos.checked_sub_signed(100), None);

// Saturating: clamps to 0 or u64::MAX.
assert_eq!(pos.saturating_sub_signed(100), 0);
assert_eq!((u64::MAX - 5).saturating_sub_signed(-100), u64::MAX);

// Wrapping: modular arithmetic, never panics.
assert_eq!(pos.wrapping_sub_signed(20), u64::MAX - 9);

// Overflowing: returns (value, did_overflow).
assert_eq!(pos.overflowing_sub_signed(20), (u64::MAX - 9, true));
assert_eq!(pos.overflowing_sub_signed(5),  (5, false));

Same convention as checked_sub, saturating_sub, etc. — you already know the shape.

Why it matters

The signed-from-unsigned case comes up more than you’d think. Scrubbing back and forth in a timeline. Applying a velocity to a position. Rebasing a byte offset. Any time the delta can be negative, you need this method — and now you have it without touching as.

It pairs nicely with its long-stable sibling checked_add_signed, which has been around since Rust 1.66. Between the two, signed deltas on unsigned counters are a one-liner in any direction.

Available on every unsigned primitive (u8, u16, u32, u64, u128, usize) as of Rust 1.91.

#078 Apr 2026

78. div_ceil — Divide and Round Up Without the Overflow Bug

Need to split items into fixed-size pages or chunks? The classic (n + size - 1) / size trick silently overflows. div_ceil does it correctly.

The classic footgun

Paging, chunking, allocating — any time you divide and need to round up, this pattern shows up:

1
2
3
fn pages_needed(total: u64, per_page: u64) -> u64 {
    (total + per_page - 1) / per_page // ⚠️ overflows when total is large
}

It works until total + per_page - 1 wraps around. With u64::MAX items and a page size of 10, you get a wrong answer instead of a panic or correct result.

The fix: div_ceil

Stabilized in Rust 1.73, div_ceil handles the rounding without intermediate overflow:

1
2
3
fn pages_needed(total: u64, per_page: u64) -> u64 {
    total.div_ceil(per_page)
}

One method call, no overflow risk, intent crystal clear.

Real-world examples

Allocating pixel rows for a tiled renderer:

1
2
3
4
let image_height: u32 = 1080;
let tile_size: u32 = 64;
let tile_rows = image_height.div_ceil(tile_size);
assert_eq!(tile_rows, 17); // 16 full tiles + 1 partial

Splitting work across threads:

1
2
3
4
let items: usize = 1000;
let threads: usize = 6;
let chunk_size = items.div_ceil(threads);
assert_eq!(chunk_size, 167); // each thread handles at most 167 items

It works on all unsigned integers

div_ceil is available on u8, u16, u32, u64, u128, and usize. Signed integers also have it (since Rust 1.73), but watch out — it rounds toward positive infinity, which for negative dividends means rounding away from zero.

1
2
let signed: i32 = -7;
assert_eq!(signed.div_ceil(2), -3); // rounds toward +∞, not toward 0

Next time you reach for (a + b - 1) / b, stop — div_ceil already exists and it won’t betray you at the boundaries.