Skip to content

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

Medium Primitives

checked, wrapping, saturating, overflowing

Sum a slice of i32, but refuse to lie about it.

pub fn safe_sum(values: &[i32]) -> Option<i32>

Return Some(total) when the running total never leaves i32, and None the moment it would. [1, 2, 3] is Some(6). [i32::MAX, 1] is None. [] is Some(0). [i32::MIN, -1] is None, because negating past the minimum overflows just as surely as adding past the maximum.

The starter compiles, passes clippy, and gets the wrong answer.

Intent in the method name, not in the build profile

The previous item left you with an uncomfortable fact: whether a + b panics or wraps depends on how the program was compiled. That is fine for catching bugs in development, and useless as a way of expressing what you want. The professional answer is that every integer type carries four families of explicit methods:

250u8.checked_add(10)      // None                — "tell me if it does not fit"
250u8.wrapping_add(10)     // 4                   — "wrap, I mean it"
250u8.saturating_add(10)   // 255                 — "clamp at the boundary"
250u8.overflowing_add(10)  // (4, true)           — "wrap, and tell me it happened"

All four exist for add, sub, mul, div, rem, neg, pow, abs and the shifts. Choosing one is a design decision that survives being read six months later, and it does not change meaning between debug and release.

Which to reach for:

  • checked_ when overflow means the input was bad and the caller must know. This is the default answer for parsing, accounting, indexing arithmetic.
  • saturating_ for clamped quantities: a health bar, a volume level, a progress percentage. “Bigger than max” genuinely means max.
  • wrapping_ for things that are supposed to be modular: hashes, checksums, ring buffers, the odometer in the previous problem.
  • overflowing_ when you need the wrapped value and the flag — most often when implementing wider arithmetic out of narrower pieces.

Verified edges worth memorising: 250u8.overflowing_add(10) == (4, true); 5u32.checked_sub(9) == None; and i32::MIN.checked_div(-1) == None, because -i32::MIN is not an i32.

Option<T> — your first sum type

checked_add returns Option<i32>, which is a value that is either Some(i32) or None. It is not a null pointer and it is not a sentinel value; it is an ordinary enum defined in the standard library:

enum Option<T> { Some(T), None }

The point is that Option<i32> and i32 are different types, so the compiler will not let you use a maybe-missing number as though it were definitely there. Every language that lacks this distinction has a null-pointer story; Rust’s is that there is no null to dereference.

? — the early-exit operator

Writing out “if it is None, return None“ for every step gets old fast, so Rust has an operator for it:

total = total.checked_add(v)?;

Read ? as: if the left-hand side is Some(x), evaluate to x and carry on; if it is None, return None from the enclosing function immediately. It only compiles in a function whose return type can carry the failure — here, Option<i32> — which is why the signature you were given is shaped the way it is. You will meet the Result version of ? in Track 6; it is the same idea.

Why the starter is wrong and looks right

total += v at -O wraps. So [i32::MAX, 1] produces -2147483648 and the function cheerfully returns Some(-2147483648). Nothing warns you. No test fails until someone feeds it a large number in production. That is the entire argument for making the fallible step visible in the type.

Once this passes, write the saturating variant in your head: change the return type to i32, swap checked_add for saturating_add, delete the ? and the Some. Same loop, different contract.

Loading visualization…