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 au8
(range 0-255),255 + 1
wraps to0
, and0 - 1
wraps to255
.
// 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 anOption<T>
. It’sSome(result)
if the operation succeeds without overflow, andNone
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, anddid_overflow
is abool
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
(orf32::INFINITY
) for positive infinity,f64::NEG_INFINITY
(orf32::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
(orf32::NAN
). This indicates an undefined or unrepresentable result, such as0.0 / 0.0
, the square root of a negative number, or arithmetic involvingNaN
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.