Index and IndexMut

How to implement the Index and IndexMut traits for custom indexing syntax in Rust, including practical examples, limitations, and common mistakes.

When you write container[key] in Rust, the compiler translates that into a call to a trait method. The two traits that make this possible are Index (for read‑only access) and IndexMut (for mutable access). Both live in std::ops and follow the same pattern as every other operator overload: implement the trait, and the syntax becomes available for your type.

The Index Trait

Index allows immutable indexing. Its definition is:

pub trait Index<Idx>
where
    Idx: ?Sized,
{
    type Output: ?Sized;
    fn index(&self, index: Idx) -> &Self::Output;
}

Idx is the type you use between the brackets. Usually it’s usize, but it could be a tuple, a custom enum, or anything else. Output is the type of the value the index expression yields — and it must be returned by reference. The ?Sized bound on Idx and Output means you can use unsized types like str or [u8] as index keys or as the output type (though the output type is almost always Sized in practice).

Implementing Index for a type gives you container[key] in rvalue positions. For example, let x = container[key]; calls Index::index.

The IndexMut Trait

IndexMut adds mutable indexing. It requires Index to be implemented first, because the trait bound is IndexMut<Idx>: Index<Idx>. The definition is:

pub trait IndexMut<Idx>: Index<Idx>
where
    Idx: ?Sized,
{
    fn index_mut(&mut self, index: Idx) -> &mut Self::Output;
}

When you write something like container[key] = value;, the compiler calls IndexMut::index_mut to obtain a mutable reference to the element, then assigns through it. The same happens for compound assignments like container[key] += 1.

IndexMut requires Index:

You cannot implement IndexMut without implementing Index first. This relationship mirrors how a mutable reference can only be obtained if an immutable one could logically exist. Omitting Index produces a compilation error.

How Rust Selects Index vs IndexMut

The choice is based on the context of the expression:

  • If the expression is used as a value (rvalue), Rust calls Index::index.
  • If the expression appears on the left-hand side of an assignment, or is passed as a &mut reference, Rust calls IndexMut::index_mut.

This means a single container[key] can be read-only or mutable depending on how it is used, exactly like a field access or a built‑in slice.

Implementing Index and IndexMut for a Simple Wrapper

The most straightforward use case is a custom type that wraps a collection but you want to expose access with brackets instead of a raw method.

use std::ops::{Index, IndexMut};
#[derive(Debug, Clone, Copy)]
pub struct Vec3 {
    e: [f32; 3],
}
impl Index<usize> for Vec3 {
    type Output = f32;
    fn index(&self, i: usize) -> &f32 {
        &self.e[i]
    }
}
impl IndexMut<usize> for Vec3 {
    fn index_mut(&mut self, i: usize) -> &mut f32 {
        &mut self.e[i]
    }
}

Now you can treat a Vec3 like a small array:

let mut point = Vec3 { e: [0.0, 1.0, 3.0] };
println!("z = {}", point[2]); // 3.0
point[0] = 99.9;

The Index implementation returns a reference to a field inside the struct. The IndexMut implementation returns a mutable reference to the same field. This pattern works because the value lives in self.e. If the element had to be created on the fly, the approach would break — a limitation we’ll cover shortly.

Everything is wired up correctly:

If you can both read my_struct[idx] and assign through my_struct[idx] = val, both traits are implemented and the compiler selected the correct version.

Indexing into a Matrix with Tuple Indices

Index types are not limited to usize. A two‑dimensional grid can accept a pair (usize, usize). The implementation stays clean and the calling code reads naturally.

use std::ops::{Index, IndexMut};
struct Matrix {
    data: Vec<f64>,
    cols: usize,
}
impl Matrix {
    fn new(rows: usize, cols: usize) -> Self {
        Self {
            data: vec![0.0; rows * cols],
            cols,
        }
    }
}
impl Index<(usize, usize)> for Matrix {
    type Output = f64;
    fn index(&self, (row, col): (usize, usize)) -> &f64 {
        &self.data[row * self.cols + col]
    }
}
impl IndexMut<(usize, usize)> for Matrix {
    fn index_mut(&mut self, (row, col): (usize, usize)) -> &mut f64 {
        &mut self.data[row * self.cols + col]
    }
}

Usage:

let mut m = Matrix::new(3, 3);
m[(1, 2)] = 5.0;
println!("{}", m[(1, 2)]); // 5.0

The tuple destructuring in the method signature extracts the row and column directly. There is no performance penalty for the custom index type; the trait call is monomorphized and the arithmetic inlines to the same machine code as a handwritten index calculation.

The Panicking Contract — Out‑of‑Bounds Access

Both index and index_mut panic if the index is out of bounds. This matches the behavior of Vec and slices. If your type cannot guarantee a valid reference for every possible index, the convention is to panic — there is no Option or Result version of the operator overload.

If you need non‑panicking access, add a separate get method:

impl Matrix {
    fn get(&self, row: usize, col: usize) -> Option<&f64> {
        if row * self.cols + col < self.data.len() {
            Some(&self.data[row * self.cols + col])
        } else {
            None
        }
    }
}

Rust users expect [] to panic on invalid input; a fallible lookup belongs in a named method.

Why IndexMut Cannot Insert New Elements

The signature of index_mut demands a &mut Self::Output that points to an existing location in memory. You cannot allocate a new slot and return a reference to it, because the reference would need to be valid without moving the collection’s internal storage. In practice this means:

  • For array‑like types, the element must already be part of the data layout.
  • For map‑like types (HashMap, BTreeMap), index_mut panics if the key is absent, just as index does.

If you want to insert a value for a missing key while using brackets, that is not possible through the IndexMut trait alone. The standard library’s HashMap does not implement IndexMut at all — it only implements Index. Insertion is handled by the entry API, which is explicit and does not pretend the element already exists.

IndexMut is not an insertion operator:

Returning &mut self.data[index] works only if data[index] already exists. If your type can dynamically grow, index_mut cannot create the new slot. For insertion, design a separate method.

Common Pitfall — Returning References to Temporaries

A common mistake when first implementing Index or IndexMut is trying to synthesize the value on the fly. For example, suppose you want to represent an array of 8 booleans as a single u8 where each bit is a boolean. You might attempt:

use std::ops::{Index, IndexMut};
struct BitVec(u8);
impl Index<u8> for BitVec {
    type Output = bool;
    fn index(&self, index: u8) -> &bool {
        let mask = 1 << index;
        if (self.0 & mask) != 0 {
            &true   // looks like it works, but why?
        } else {
            &false
        }
    }
}

This Index implementation compiles under certain conditions because the boolean literals true and false can be promoted to static references (&'static bool). However, the reference does not point into self — it points to a read‑only constant that never changes. Reading works accidentally, but the value does not reflect the actual bit state if you later mutate the underlying u8.

The IndexMut version fails outright:

impl IndexMut<u8> for BitVec {
    fn index_mut(&mut self, index: u8) -> &mut bool {
        let mask = 1 << index;
        if (self.0 & mask) != 0 {
            &mut true  // error[E0515]: cannot return reference to temporary
        } else {
            &mut false
        }
    }
}

Rust rejects this because &mut true is a mutable reference to a temporary value that does not live long enough. More fundamentally, there is no mutable bool stored anywhere in BitVec that can be borrowed.

Always return a reference into self:

Every implementation of Index and IndexMut must return a reference that points directly into self (or into some memory self owns that is not going to be freed during the borrow). Synthesized or intermediate values will not work.

Performance Considerations

The indexing operator desugars to a trait method call, but because Rust monomorphizes generics, the compiler sees the concrete types at compile time. The function call is usually inlined, resulting in machine code identical to a direct array access. There is no virtual dispatch, no allocation, and no hidden cost. The only runtime overhead is whatever bounds checking you choose to include — and you decide whether to do that check or trust the caller (just like the standard library does for Vec).

When to Implement Index and IndexMut vs Using Named Methods

Bracket syntax is most appropriate when your type is conceptually a collection and indexing is its primary way of accessing elements. Good fits:

  • A Matrix or grid that maps (row, col) to an element.
  • A wrapper around a Vec that exposes only a portion of its API.
  • A bit‑set where indexing reads or writes individual bits (provided you store real bool values, not synthesized ones).

Avoid implementing Index or IndexMut when the operation behind the brackets is expensive, has side effects, or does not naturally return a reference to stable storage. A named method like .get() or .entry() gives the caller better control and makes the cost explicit.

Summary

Index and IndexMut turn container[key] into ordinary trait calls. They follow the same rules as every other operator overload: implement the trait, and the syntax works. The implementation must return a genuine reference into self; you cannot synthesize the value on the fly. IndexMut requires Index to be present, and it gives mutable access to an element that already exists — it is not a mechanism for inserting new data.