We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ground Rules: Values, Types, Control Flow step 6 of 24
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…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.