How to Use std

:num for Number-Related Traits (NonZero, Wrapping, etc.)

Import types from std::num to enforce non-zero values or enable wrapping arithmetic safely.

When zero breaks your logic

You are building a cache. The key is a u32. You need a special value to mean "no key yet." You pick 0. Later, you realize 0 is a valid key for the first item. Now you are juggling Option<u32> or inventing a sentinel like u32::MAX. Both feel messy. Option wastes space. Sentinels create magic numbers that confuse readers.

Rust has a better way. The std::num module provides wrapper types that bake constraints directly into the type system. NonZeroU32 guarantees the value is never zero. Wrapping<u8> guarantees arithmetic wraps around instead of panicking. Saturating<i32> guarantees values clamp to limits. These types turn runtime bugs into compile-time errors.

Wrapper types that enforce rules

Standard integers like u32 or i32 are flexible. They hold any value in their range. That flexibility is dangerous when your domain has stricter rules. A user ID should never be zero. A volume slider should never exceed 100. A checksum calculation should wrap on overflow.

std::num types enforce these rules at the type level. The compiler checks the rules for you. If you try to create a NonZeroU32 with zero, the code does not compile. If you try to add two Wrapping values, the result wraps silently. The type tells the reader exactly what to expect.

Think of these types as smart containers. A u32 is a box that holds any number. NonZeroU32 is a box with a lock that only accepts non-zero keys. If you try to put zero in, the lock jams. The compiler is the mechanic who refuses to hand you the key until you fix the input.

NonZero: The zero-proof integer

NonZero types exist for every integer size: NonZeroU8, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU128, and the signed variants. They work the same way. The constructor returns an Option. A valid value returns Some. Zero returns None.

use std::num::NonZeroU32;

fn main() {
    // NonZeroU32::new returns Option<NonZeroU32>.
    // The compiler forces you to handle the case where the value is zero.
    let id = NonZeroU32::new(42).expect("ID cannot be zero");

    // This would fail to compile if you passed 0.
    // let bad_id = NonZeroU32::new(0).expect("...");

    println!("Valid ID: {}", id);
}

The expect call panics if the value is zero. In production code, you usually return an error or use if let to handle the failure gracefully. The point is the compiler makes you think about zero. You cannot accidentally create a zero value.

Convention aside: NonZero types are the secret weapon for HashMap performance. When you use NonZero as a key, the hash map can use a more compact internal representation. The map uses zero as a marker for empty slots. Since the key can never be zero, the map packs data tighter and skips extra checks. This reduces memory usage and speeds up lookups. If you are storing integers in a hash map and zero is invalid, switch to NonZero. The performance gain is real.

Wrapping and Saturating: Choosing overflow behavior

Rust panics on integer overflow in debug builds. This is safe but sometimes wrong. A circular buffer index should wrap. A volume counter should clamp. std::num gives you types that define the overflow behavior upfront.

Wrapping<T> implements arithmetic traits so that overflow wraps around. 255u8 + 1 becomes 0. No panic. No silent truncation in release mode. The behavior is consistent across all build profiles.

Saturating<T> implements arithmetic so that overflow clamps to the maximum or minimum value. 255u8 + 1 stays 255. -128i8 - 1 stays -128. This is useful for UI counters, progress bars, and any logic where hitting a limit should stop the value from jumping to the other side.

use std::num::{Wrapping, Saturating};

fn main() {
    // Wrapping arithmetic cycles like a clock.
    // 255 + 1 wraps to 0.
    let wrapped = Wrapping(255u8) + Wrapping(1u8);

    // Saturating arithmetic clamps at the limit.
    // 255 + 1 stays 255.
    let saturated = Saturating(255u8) + Saturating(1u8);

    println!("Wrapped: {}, Saturated: {}", wrapped.0, saturated.0);
}

Convention aside: Use Wrapping for low-level bit manipulation, checksums, and circular buffers. Use Saturating for high-level logic where clamping makes sense, like volume controls or progress indicators. Never use Wrapping when you expect the value to grow unbounded. Wrapping hides overflow bugs. Pick the type that matches your domain.

Minimal example

This example shows how to use NonZero and Wrapping together. It demonstrates the constructor pattern and the arithmetic behavior.

use std::num::{NonZeroU32, Wrapping};

/// Calculates a wrapped counter and validates a non-zero ID.
fn process(id_input: u32, increment: u8) -> Option<(NonZeroU32, u8)> {
    // Convert the input to NonZeroU32.
    // Returns None if id_input is zero.
    let id = NonZeroU32::new(id_input)?;

    // Create a wrapping counter starting at 250.
    let mut counter = Wrapping(250u8);

    // Add the increment.
    // If increment is 10, 250 + 10 = 260, which wraps to 4.
    counter += Wrapping(increment);

    // Return the validated ID and the wrapped counter value.
    Some((id, counter.0))
}

fn main() {
    // Valid ID, counter wraps.
    if let Some((id, count)) = process(42, 10) {
        println!("ID: {}, Count: {}", id, count);
    }

    // Invalid ID, returns None.
    if let Some((id, count)) = process(0, 10) {
        println!("This won't print");
    } else {
        println!("Rejected zero ID");
    }
}

The ? operator in process propagates the None if the ID is zero. This keeps the function clean. The Wrapping addition handles the overflow automatically. The result is a Wrapping<u8>, so you access the inner value with .0.

Realistic example: Entity management

Game engines and simulations often use entity IDs. Zero is usually reserved for "no entity." You want to store entities in a map and ensure IDs are valid. NonZeroU32 is perfect here.

use std::num::NonZeroU32;
use std::collections::HashMap;

/// Stores entity data keyed by a non-zero ID.
/// Using NonZeroU32 allows the HashMap to use a compact representation.
struct EntityStore {
    entities: HashMap<NonZeroU32, String>,
}

impl EntityStore {
    fn new() -> Self {
        Self {
            entities: HashMap::new(),
        }
    }

    /// Adds an entity. Returns false if the ID is zero.
    fn add(&mut self, id: u32, name: String) -> bool {
        // Try to convert u32 to NonZeroU32.
        // If id is 0, this returns None and we reject the insert.
        if let Some(nz_id) = NonZeroU32::new(id) {
            self.entities.insert(nz_id, name);
            true
        } else {
            false
        }
    }

    /// Gets an entity by ID.
    fn get(&self, id: NonZeroU32) -> Option<&String> {
        self.entities.get(&id)
    }
}

fn main() {
    let mut store = EntityStore::new();

    // Add valid entities.
    assert!(store.add(1, "Player".to_string()));
    assert!(store.add(2, "Enemy".to_string()));

    // Attempt to add zero ID. Fails safely.
    assert!(!store.add(0, "Invalid".to_string()));

    // Retrieve an entity.
    let id = NonZeroU32::new(1).unwrap();
    println!("Entity: {:?}", store.get(id));
}

The add method uses if let to handle the conversion. If the ID is zero, the function returns false. The map never stores an invalid key. The get method takes a NonZeroU32, so the caller must have a valid ID. The type system prevents passing zero to get.

Pitfalls and compiler errors

NonZero types have friction points. You cannot perform arithmetic directly on them. The + operator is not implemented for NonZeroU32. If you try to add two NonZero values, the compiler rejects you with E0277 (the trait bound NonZeroU32: Add is not satisfied). You must convert to the base integer, do the math, and convert back.

use std::num::NonZeroU32;

fn main() {
    let a = NonZeroU32::new(10).unwrap();
    let b = NonZeroU32::new(20).unwrap();

    // This fails with E0277.
    // let sum = a + b;

    // Correct approach: convert, add, convert back.
    let sum = NonZeroU32::new(a.get() + b.get());
}

The result of arithmetic might be zero. 1 - 1 is 0. NonZero cannot hold zero. The conversion back to NonZero returns Option. You must handle the case where the result is zero.

NonZero does not prevent overflow. NonZeroU32::new(u32::MAX).unwrap() + 1 overflows. The type only guarantees the value is not zero. It does not guarantee the value is within a safe range for arithmetic. Use Wrapping or Saturating if you need overflow control.

You cannot pass a NonZeroU32 where a u32 is expected without conversion. The compiler rejects this with E0308 (mismatched types). Use .get() to extract the inner value.

fn takes_u32(val: u32) {
    println!("{}", val);
}

fn main() {
    let nz = NonZeroU32::new(42).unwrap();

    // This fails with E0308.
    // takes_u32(nz);

    // Correct: extract the value.
    takes_u32(nz.get());
}

Convention aside: Never use NonZeroU32::new_unchecked unless you have a mathematical proof the value is non-zero. This function is unsafe. It skips the check. If you pass zero, you get undefined behavior. The compiler cannot verify the safety. Reserve new_unchecked for FFI boundaries where you trust the external code, or for performance-critical loops where the check is redundant and proven. In 99% of cases, use new.

Treat the type as a contract. If the value can be zero, the type system will stop you at the source. Do not fight the compiler with unwrap. Handle the Option.

Decision matrix

Use NonZero when you need to distinguish zero from valid data, or when you want HashMap performance optimizations for integer keys. Use NonZero for FFI handles that are guaranteed non-null, like file descriptors or socket IDs. Use Wrapping when you expect overflow and want it to wrap silently without panicking, like in checksums, hash functions, or circular buffers. Use Saturating when you want overflow to clamp to the max or min value, like in UI counters, volume controls, or progress bars. Use plain integers when you want the compiler to panic on overflow in debug builds, which is the default safe behavior for most logic. Use Option<T> when the value might be absent, not just zero. NonZero is for "present but not zero," not "maybe absent."

Pick the overflow behavior that matches your domain. Wrapping is for math that cycles. Saturating is for limits. Panicking is for bugs.

Where to go next