Arithmetic

#167 May 2026

167. Saturating<T> — Stop Calling .saturating_add() Everywhere

When every step in a loop or formula could overflow, calling .saturating_add() and .saturating_sub() on each one turns one line of math into a paragraph.

std::num::Saturating<T> is a tuple newtype wrapper that overloads the normal operators (+, -, *, +=, …) to use saturating semantics — operations clamp to the type’s MIN or MAX instead of wrapping or panicking. You write a + b, not a.saturating_add(b).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use std::num::Saturating;

let hp: Saturating<u8> = Saturating(250);
let heal = Saturating(20u8);
let damage = Saturating(255u8);

// Pins at 255 instead of wrapping or panicking.
let healed = hp + heal;
assert_eq!(healed.0, 255);

// Pins at 0 instead of underflowing.
let dead = hp - damage;
assert_eq!(dead.0, 0);

The unwrapped equivalent works, but every operator turns into a method call:

1
2
3
4
5
let hp: u8 = 250;
let healed = hp.saturating_add(20);
let dead = hp.saturating_sub(255);
assert_eq!(healed, 255);
assert_eq!(dead, 0);

The wrapper really pays off inside a formula or an iterator chain, where you’d otherwise be wrapping each binary op:

1
2
3
4
5
use std::num::Saturating;

let damages = [Saturating(200u8), Saturating(100), Saturating(50)];
let total: Saturating<u8> = damages.iter().copied().sum(); // sticks at 255
assert_eq!(total.0, 255);

There’s a matching std::num::Wrapping<T> when you actually want cyclical math (hashes, CRCs, monotonic counters that should roll over), and both wrappers implement From, Default, Sum, and Product, so you can drop them into structs and iterator chains without ceremony.

Reach for Saturating<T> whenever a value has a logical floor or ceiling — health bars, progress percentages, retry budgets — and overflow should pin to the edge instead of panicking in debug or silently wrapping in release.

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.