How to use windows and chunks on slices

Use `windows()` to iterate over overlapping sub-slices of a fixed size, and `chunks()` to iterate over non-overlapping sub-slices of a fixed size.

The sliding view versus the fixed slice

You are processing a stream of sensor readings and need to calculate a moving average. Or you are parsing a binary protocol where each packet is exactly sixteen bytes. Or you are scanning a DNA sequence for overlapping genetic markers. In each case, you have a contiguous block of data and you need to break it into smaller pieces. The question is whether those pieces should overlap or sit side by side.

Rust slices give you two clean answers. windows(n) hands you overlapping sub-slices that slide forward by one element at a time. chunks(n) hands you non-overlapping sub-slices that step forward by n elements. Both return iterators that yield &[T] references. You get batch processing without manual index math, without heap allocations, and without copying data.

How the two approaches differ

Think of a long strip of film. windows(n) is a camera mounted on a track. The lens captures a frame of width n. The track moves forward by exactly one frame, and the lens captures the next frame. The frames overlap heavily. You see every possible contiguous sequence of length n.

chunks(n) is a pair of scissors. You cut a piece of width n, set it aside, and cut the next piece right where the last one ended. The pieces never overlap. If the strip does not divide evenly, the final piece is simply shorter.

chunks_exact(n) is a factory stamping machine. It only accepts blocks of exactly width n. If the remaining material is too short, the machine discards it. You get perfectly sized batches, but you lose the remainder.

All three methods work on any slice type &[T]. They return standard Rust iterators. You can chain them with map, filter, fold, or collect. The compiler turns them into tight loops that touch memory exactly once per element.

A minimal example

fn main() {
    let data = [1, 2, 3, 4, 5, 6];

    // windows(3) slides forward by one, yielding overlapping slices
    // Each iteration gives a reference to a contiguous sub-slice
    for window in data.windows(3) {
        // Summing demonstrates that we get a real &[i32] we can iterate
        let total: i32 = window.iter().sum();
        println!("Window sum: {}", total);
    }

    // chunks(3) steps forward by three, yielding separate slices
    // The last chunk may be smaller if the length is not a multiple
    for chunk in data.chunks(3) {
        let product: i32 = chunk.iter().product();
        println!("Chunk product: {}", product);
    }

    // chunks_exact(3) behaves like chunks but drops any remainder
    // Use this when downstream code strictly expects fixed-size input
    for exact in data.chunks_exact(3) {
        println!("Exact chunk: {:?}", exact);
    }
}

Run this and you will see windows produce four overlapping groups, chunks produce two complete groups, and chunks_exact produce the same two groups while silently ignoring anything that does not fit. The output matches the mental model exactly.

What happens under the hood

A Rust slice is a fat pointer. It holds a memory address and a length. When you call data.windows(3), the method does not allocate a new array. It returns an iterator struct that stores the original pointer, the original length, and the window size.

On each call to next(), the iterator advances the pointer by one byte (or one element, depending on T), decrements the remaining length, and returns a new slice pointing at the current position with length n. The compiler inlines this logic. There is no heap allocation. There is no bounds checking beyond the initial validation. The CPU prefetches the memory sequentially.

chunks(n) works the same way, except the pointer advances by n * size_of::<T>() on each step. chunks_exact(n) stops the iterator early when the remaining length drops below n.

This is why these methods are called zero-cost abstractions. You write high-level iterator chains, and the generated machine code looks like a hand-written C loop with manual pointer arithmetic. The borrow checker guarantees you never read past the end of the slice, and the iterator guarantees you never skip or duplicate elements unless you explicitly ask for it.

Real-world batch processing

Batch processing rarely stops at a simple for loop. You usually need to transform, filter, or aggregate the results. Here is how you might process a byte stream into fixed-size packets, compute a checksum for each, and discard incomplete trailing data.

fn process_packets(data: &[u8]) -> Vec<u16> {
    // chunks_exact guarantees every slice is exactly 16 bytes
    // The remainder is intentionally dropped to avoid partial packet corruption
    let packet_iter = data.chunks_exact(16);

    // map transforms each packet into a simple checksum
    // We fold over the bytes to avoid allocating intermediate vectors
    let checksums = packet_iter.map(|packet| {
        packet.iter().fold(0u16, |acc, &byte| acc.wrapping_add(byte as u16))
    });

    // collect gathers the results into a heap-allocated vector
    // The iterator chain runs lazily until this point
    checksums.collect()
}

Notice the convention here. When you call chunks_exact, you are making a deliberate choice to ignore the remainder. If you need to handle the leftover bytes, chain chunks_exact with into_remainder(). The method returns the trailing slice that did not fit. This pattern keeps your hot loop tight while giving you explicit control over edge cases.

Another common pattern is in-place mutation. If you need to normalize or encrypt each batch, reach for chunks_mut(n) instead. It yields &mut [T] slices. The borrow checker enforces that each mutable chunk is disjoint, so you can safely mutate the data without temporary allocations.

fn normalize_in_place(data: &mut [f32]) {
    // chunks_mut yields non-overlapping mutable slices
    // Each batch is processed independently without aliasing
    for chunk in data.chunks_mut(4) {
        let max = chunk.iter().copied().fold(f32::NEG_INFINITY, f32::max);
        // Scale each element by the local maximum
        for val in chunk.iter_mut() {
            *val /= max;
        }
    }
}

The compiler rejects any attempt to hold references across chunk boundaries. You get parallelizable, cache-friendly mutation with zero runtime overhead. Trust the borrow checker here. It prevents data races before they happen.

Where things go wrong

The most common mistake is assuming chunks always yields slices of length n. It does not. The final slice shrinks to fit the remainder. If your downstream code indexes into the slice assuming a fixed size, you will get a panic at runtime. The compiler cannot catch this because slice lengths are dynamic.

let data = [1, 2, 3, 4, 5];
for chunk in data.chunks(3) {
    // This panics on the second iteration because chunk.len() is 2
    let _ = chunk[2]; 
}

The fix is either to check chunk.len() before indexing, or to switch to chunks_exact if you want the remainder discarded automatically. If you need the remainder, use chunks_exact followed by into_remainder().

Another trap is passing 0 as the chunk or window size. Both methods panic immediately with thread 'main' panicked at 'chunk size must be non-zero'. The compiler does not catch this at compile time because n is usually a runtime variable. Validate your input before calling the method.

If you try to mutate data through chunks or windows, the compiler rejects you with E0596 (cannot borrow as mutable, as it is not declared mutable) or E0277 (trait bound not satisfied). The methods return shared references by design. Switch to chunks_mut or windows_mut when mutation is required. The windows_mut variant exists but is rarely used because overlapping mutable slices would violate Rust's aliasing rules. The language simply does not allow it.

One more subtle issue: iterator laziness. Calling data.windows(3).map(|w| w.iter().sum()) does not compute anything until you consume the iterator. If you forget to call collect(), for_each(), or similar, your code compiles but does nothing. This is a standard Rust iterator trait. Chain deliberately. Consume explicitly.

Picking the right tool

Use windows(n) when you need overlapping sequences for sliding calculations, pattern matching, or rolling statistics. Use chunks(n) when you want to process data in fixed-size batches and are prepared to handle a smaller final slice. Use chunks_exact(n) when your algorithm strictly requires uniform batch sizes and you can safely discard the remainder. Use chunks_mut(n) when you need to transform or normalize data in place without allocating temporary buffers. Reach for into_remainder() when you must process the leftover elements after exact chunking. Stick to shared references unless mutation is explicitly required.

Where to go next