5.8 Overflow in Arithmetic Operations

Integer overflow occurs when an arithmetic operation results in a value outside the representable range for its type. C/C++ behavior for signed overflow is often undefined, leading to subtle bugs and security vulnerabilities. Rust provides well-defined, safer behavior.

  • Debug Builds: By default, when compiling in debug mode (cargo build), Rust inserts runtime checks for integer overflow. If an operation (like +, -, *) overflows, the program will panic (terminate with an error message). This helps catch potential overflow errors during development and testing.
  • Release Builds: By default, when compiling in release mode (cargo build --release), these runtime checks are disabled for performance. Instead, integer operations that overflow will perform two’s complement wrapping. For example, for a u8 (range 0-255), 255 + 1 wraps to 0, and 0 - 1 wraps to 255.
// Example (behavior depends on build mode: debug vs release)
fn main() {
   let max_u8: u8 = 255;

   // This line's behavior changes:
   // - Debug: Panics with "attempt to add with overflow"
   // - Release: Wraps around, result becomes 0
   let result = max_u8 + 1;

   println!("Result: {}", result); // Only runs in release mode without panic
}

This difference means code relying on wrapping behavior might panic unexpectedly in debug builds, while code assuming panics won’t happen might produce incorrect results due to wrapping in release builds.

5.8.1 Explicit Overflow Handling

To ensure consistent and predictable behavior regardless of build mode, Rust provides methods on integer types for explicit overflow control:

  • Wrapping: Methods like wrapping_add, wrapping_sub, wrapping_mul, etc., always perform two’s complement wrapping, in both debug and release builds.
    #![allow(unused)]
    fn main() {
    let x: u8 = 250;
    let y = x.wrapping_add(10); // Always wraps: 250+10 -> 260 -> 4 (mod 256). y is 4.
    }
  • Checked: Methods like checked_add, checked_sub, etc., perform the operation and return an Option<T>. It’s Some(result) if the operation succeeds without overflow, and None if overflow occurs. This allows you to detect and handle overflow explicitly.
    #![allow(unused)]
    fn main() {
    let x: u8 = 250;
    let sum1 = x.checked_add(5);  // Some(255)
    let sum2 = x.checked_add(10); // None (because 250 + 10 > 255)
    
    if let Some(value) = sum2 {
        println!("Checked sum succeeded: {}", value);
    } else {
        println!("Checked sum overflowed!"); // This branch is taken
    }
    }
  • Saturating: Methods like saturating_add, saturating_sub, etc., perform the operation, but if overflow occurs, the result is clamped (“saturated”) at the numeric type’s minimum or maximum value.
    #![allow(unused)]
    fn main() {
    let x: u8 = 250;
    let sum = x.saturating_add(10); // Clamps at u8::MAX (255). sum is 255.
    let y: i8 = -120;
    let diff = y.saturating_sub(20); // Clamps at i8::MIN (-128). diff is -128.
    }
  • Overflowing: Methods like overflowing_add, overflowing_sub, etc., perform the operation using wrapping semantics and return a tuple (result, did_overflow). result contains the wrapped value, and did_overflow is a bool indicating whether wrapping occurred.
    #![allow(unused)]
    fn main() {
    let x: u8 = 250;
    let (sum, overflowed) = x.overflowing_add(10); // sum is 4 (wrapped), overfl. is true
    println!("Overflowing sum: {}, Overflowed: {}", sum, overflowed);
    }

Choose the method that best reflects the intended logic for calculations that might exceed the type’s bounds. Relying on the default build-mode-dependent behavior is often risky.

5.8.2 Floating-Point Overflow

Floating-point types (f32, f64) adhere to the IEEE 754 standard for arithmetic and do not panic or wrap on overflow. Instead, operations exceeding representable limits produce special values:

  • Infinity: f64::INFINITY (or f32::INFINITY) for positive infinity, f64::NEG_INFINITY (or f32::NEG_INFINITY) for negative infinity. This typically results from dividing by zero or calculations producing results of enormous magnitude.
  • NaN (Not a Number): f64::NAN (or f32::NAN). This indicates an undefined or unrepresentable result, such as 0.0 / 0.0, the square root of a negative number, or arithmetic involving NaN itself.
fn main() {
    let x = 1.0f64 / 0.0; // Positive Infinity
    let y = -1.0f64 / 0.0; // Negative Infinity
    let z = 0.0f64 / 0.0; // NaN

    println!("x = {}, y = {}, z = {}", x, y, z);

    // Use methods to check for these special values
    println!("x is infinite: {}", x.is_infinite()); // true
    println!("x is finite: {}", x.is_finite());   // false
    println!("y is infinite: {}", y.is_infinite()); // true
    println!("z is NaN: {}", z.is_nan());         // true

    // Crucial NaN comparison behavior: NaN is not equal to anything, including itself!
    println!("z == z: {}", z == z); // false! Use is_nan() instead.
}

Code involving floating-point arithmetic should be prepared to handle Infinity and especially NaN. Remember that direct equality checks (==) with NaN always return false; use the .is_nan() method instead.